@ttdk-design-system/library-vue 0.0.0-stage → 1.0.0

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 (232) hide show
  1. package/AI_GUIDE.md +6518 -0
  2. package/llms.txt +6518 -0
  3. package/package.json +1096 -4
  4. package/src/core/index.ts +2 -0
  5. package/src/core/tailwind-preset.cjs +62 -0
  6. package/src/core/tailwind-preset.ts +85 -0
  7. package/src/core/theme/tokens.css +663 -0
  8. package/src/core/types.ts +836 -0
  9. package/src/index.ts +17 -0
  10. package/src/tailwind-preset.cjs +62 -0
  11. package/src/tailwind-preset.ts +85 -0
  12. package/src/types.ts +1 -0
  13. package/src/web/accordion/AI_GUIDE.md +149 -0
  14. package/src/web/accordion/accordion.css +1 -0
  15. package/src/web/accordion/accordion.vue +432 -0
  16. package/src/web/accordion/accordion.vue.d.ts +227 -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.vue +290 -0
  22. package/src/web/alert/alert.vue.d.ts +154 -0
  23. package/src/web/alert/index.ts +3 -0
  24. package/src/web/alert/manifest.json +240 -0
  25. package/src/web/avatar/AI_GUIDE.md +118 -0
  26. package/src/web/avatar/avatar.css +1 -0
  27. package/src/web/avatar/avatar.vue +217 -0
  28. package/src/web/avatar/avatar.vue.d.ts +135 -0
  29. package/src/web/avatar/index.ts +3 -0
  30. package/src/web/avatar/manifest.json +195 -0
  31. package/src/web/badge/AI_GUIDE.md +94 -0
  32. package/src/web/badge/badge.css +1 -0
  33. package/src/web/badge/badge.vue +135 -0
  34. package/src/web/badge/badge.vue.d.ts +108 -0
  35. package/src/web/badge/index.ts +3 -0
  36. package/src/web/badge/manifest.json +141 -0
  37. package/src/web/banner/AI_GUIDE.md +130 -0
  38. package/src/web/banner/banner.css +1 -0
  39. package/src/web/banner/banner.vue +303 -0
  40. package/src/web/banner/banner.vue.d.ts +162 -0
  41. package/src/web/banner/index.ts +3 -0
  42. package/src/web/banner/manifest.json +240 -0
  43. package/src/web/breadcrumb/AI_GUIDE.md +120 -0
  44. package/src/web/breadcrumb/breadcrumb.css +1 -0
  45. package/src/web/breadcrumb/breadcrumb.vue +313 -0
  46. package/src/web/breadcrumb/breadcrumb.vue.d.ts +139 -0
  47. package/src/web/breadcrumb/index.ts +3 -0
  48. package/src/web/breadcrumb/manifest.json +225 -0
  49. package/src/web/button/AI_GUIDE.md +121 -0
  50. package/src/web/button/button.css +1 -0
  51. package/src/web/button/button.vue +283 -0
  52. package/src/web/button/button.vue.d.ts +147 -0
  53. package/src/web/button/index.ts +3 -0
  54. package/src/web/button/manifest.json +177 -0
  55. package/src/web/calendar/AI_GUIDE.md +151 -0
  56. package/src/web/calendar/calendar.css +1 -0
  57. package/src/web/calendar/calendar.vue +1536 -0
  58. package/src/web/calendar/calendar.vue.d.ts +261 -0
  59. package/src/web/calendar/index.ts +3 -0
  60. package/src/web/calendar/manifest.json +470 -0
  61. package/src/web/card/AI_GUIDE.md +133 -0
  62. package/src/web/card/card.css +1 -0
  63. package/src/web/card/card.vue +423 -0
  64. package/src/web/card/card.vue.d.ts +232 -0
  65. package/src/web/card/index.ts +3 -0
  66. package/src/web/card/manifest.json +357 -0
  67. package/src/web/checkbox/AI_GUIDE.md +146 -0
  68. package/src/web/checkbox/checkbox.css +1 -0
  69. package/src/web/checkbox/checkbox.vue +304 -0
  70. package/src/web/checkbox/checkbox.vue.d.ts +189 -0
  71. package/src/web/checkbox/index.ts +3 -0
  72. package/src/web/checkbox/manifest.json +294 -0
  73. package/src/web/chip/AI_GUIDE.md +186 -0
  74. package/src/web/chip/chip.css +1 -0
  75. package/src/web/chip/chip.vue +220 -0
  76. package/src/web/chip/chip.vue.d.ts +140 -0
  77. package/src/web/chip/index.ts +3 -0
  78. package/src/web/chip/manifest.json +186 -0
  79. package/src/web/dropdown/AI_GUIDE.md +167 -0
  80. package/src/web/dropdown/dropdown.css +1 -0
  81. package/src/web/dropdown/dropdown.vue +1257 -0
  82. package/src/web/dropdown/dropdown.vue.d.ts +381 -0
  83. package/src/web/dropdown/index.ts +3 -0
  84. package/src/web/dropdown/manifest.json +576 -0
  85. package/src/web/flex/AI_GUIDE.md +113 -0
  86. package/src/web/flex/flex-item.vue +94 -0
  87. package/src/web/flex/flex-item.vue.d.ts +84 -0
  88. package/src/web/flex/flex.css +1 -0
  89. package/src/web/flex/flex.vue +103 -0
  90. package/src/web/flex/flex.vue.d.ts +92 -0
  91. package/src/web/flex/index.ts +5 -0
  92. package/src/web/flex/manifest.json +162 -0
  93. package/src/web/form/index.ts +7 -0
  94. package/src/web/grid/AI_GUIDE.md +112 -0
  95. package/src/web/grid/grid-item.vue +109 -0
  96. package/src/web/grid/grid-item.vue.d.ts +99 -0
  97. package/src/web/grid/grid.css +1 -0
  98. package/src/web/grid/grid.vue +94 -0
  99. package/src/web/grid/grid.vue.d.ts +84 -0
  100. package/src/web/grid/index.ts +5 -0
  101. package/src/web/grid/manifest.json +153 -0
  102. package/src/web/index.ts +86 -0
  103. package/src/web/input/AI_GUIDE.md +159 -0
  104. package/src/web/input/index.ts +3 -0
  105. package/src/web/input/input.css +1 -0
  106. package/src/web/input/input.vue +521 -0
  107. package/src/web/input/input.vue.d.ts +274 -0
  108. package/src/web/input/manifest.json +465 -0
  109. package/src/web/input-number/AI_GUIDE.md +164 -0
  110. package/src/web/input-number/index.ts +3 -0
  111. package/src/web/input-number/input-number.css +1 -0
  112. package/src/web/input-number/input-number.vue +1153 -0
  113. package/src/web/input-number/input-number.vue.d.ts +343 -0
  114. package/src/web/input-number/manifest.json +575 -0
  115. package/src/web/modal/AI_GUIDE.md +171 -0
  116. package/src/web/modal/index.ts +3 -0
  117. package/src/web/modal/manifest.json +375 -0
  118. package/src/web/modal/modal.css +1 -0
  119. package/src/web/modal/modal.vue +344 -0
  120. package/src/web/modal/modal.vue.d.ts +64 -0
  121. package/src/web/pagination/AI_GUIDE.md +148 -0
  122. package/src/web/pagination/index.ts +21 -0
  123. package/src/web/pagination/manifest.json +432 -0
  124. package/src/web/pagination/pagination-content.vue +42 -0
  125. package/src/web/pagination/pagination-content.vue.d.ts +28 -0
  126. package/src/web/pagination/pagination-ellipsis.vue +55 -0
  127. package/src/web/pagination/pagination-ellipsis.vue.d.ts +28 -0
  128. package/src/web/pagination/pagination-first.vue +161 -0
  129. package/src/web/pagination/pagination-first.vue.d.ts +67 -0
  130. package/src/web/pagination/pagination-item.vue +42 -0
  131. package/src/web/pagination/pagination-item.vue.d.ts +28 -0
  132. package/src/web/pagination/pagination-last.vue +161 -0
  133. package/src/web/pagination/pagination-last.vue.d.ts +67 -0
  134. package/src/web/pagination/pagination-link.vue +118 -0
  135. package/src/web/pagination/pagination-link.vue.d.ts +61 -0
  136. package/src/web/pagination/pagination-next.vue +160 -0
  137. package/src/web/pagination/pagination-next.vue.d.ts +68 -0
  138. package/src/web/pagination/pagination-previous.vue +160 -0
  139. package/src/web/pagination/pagination-previous.vue.d.ts +68 -0
  140. package/src/web/pagination/pagination.css +1 -0
  141. package/src/web/pagination/pagination.vue +730 -0
  142. package/src/web/pagination/pagination.vue.d.ts +247 -0
  143. package/src/web/popover/AI_GUIDE.md +163 -0
  144. package/src/web/popover/index.ts +3 -0
  145. package/src/web/popover/manifest.json +396 -0
  146. package/src/web/popover/popover.css +1 -0
  147. package/src/web/popover/popover.vue +579 -0
  148. package/src/web/popover/popover.vue.d.ts +83 -0
  149. package/src/web/radio/AI_GUIDE.md +155 -0
  150. package/src/web/radio/index.ts +3 -0
  151. package/src/web/radio/manifest.json +294 -0
  152. package/src/web/radio/radio.css +1 -0
  153. package/src/web/radio/radio.vue +288 -0
  154. package/src/web/radio/radio.vue.d.ts +182 -0
  155. package/src/web/selectbox/AI_GUIDE.md +216 -0
  156. package/src/web/selectbox/index.ts +3 -0
  157. package/src/web/selectbox/manifest.json +477 -0
  158. package/src/web/selectbox/selectbox.css +1 -0
  159. package/src/web/selectbox/selectbox.vue +1024 -0
  160. package/src/web/selectbox/selectbox.vue.d.ts +300 -0
  161. package/src/web/sonner/AI_GUIDE.md +98 -0
  162. package/src/web/sonner/index.ts +114 -0
  163. package/src/web/sonner/manifest.json +132 -0
  164. package/src/web/sonner/sonner.css +1 -0
  165. package/src/web/sonner/sonner.vue +180 -0
  166. package/src/web/sonner/sonner.vue.d.ts +113 -0
  167. package/src/web/table/AI_GUIDE.md +145 -0
  168. package/src/web/table/index.ts +34 -0
  169. package/src/web/table/manifest.json +168 -0
  170. package/src/web/table/table-body.vue +42 -0
  171. package/src/web/table/table-body.vue.d.ts +28 -0
  172. package/src/web/table/table-caption.vue +42 -0
  173. package/src/web/table/table-caption.vue.d.ts +28 -0
  174. package/src/web/table/table-cell.vue +71 -0
  175. package/src/web/table/table-cell.vue.d.ts +49 -0
  176. package/src/web/table/table-empty.vue +105 -0
  177. package/src/web/table/table-empty.vue.d.ts +57 -0
  178. package/src/web/table/table-footer.vue +42 -0
  179. package/src/web/table/table-footer.vue.d.ts +28 -0
  180. package/src/web/table/table-head.vue +78 -0
  181. package/src/web/table/table-head.vue.d.ts +50 -0
  182. package/src/web/table/table-header.vue +42 -0
  183. package/src/web/table/table-header.vue.d.ts +28 -0
  184. package/src/web/table/table-row.vue +70 -0
  185. package/src/web/table/table-row.vue.d.ts +44 -0
  186. package/src/web/table/table.css +1 -0
  187. package/src/web/table/table.vue +298 -0
  188. package/src/web/table/table.vue.d.ts +119 -0
  189. package/src/web/tabs/AI_GUIDE.md +150 -0
  190. package/src/web/tabs/index.ts +2 -0
  191. package/src/web/tabs/manifest.json +315 -0
  192. package/src/web/tabs/tabs.css +1 -0
  193. package/src/web/tabs/tabs.vue +164 -0
  194. package/src/web/tabs/tabs.vue.d.ts +60 -0
  195. package/src/web/text/AI_GUIDE.md +129 -0
  196. package/src/web/text/index.ts +2 -0
  197. package/src/web/text/manifest.json +195 -0
  198. package/src/web/text/text.css +1 -0
  199. package/src/web/text/text.vue +195 -0
  200. package/src/web/text/text.vue.d.ts +161 -0
  201. package/src/web/textarea/AI_GUIDE.md +141 -0
  202. package/src/web/textarea/index.ts +3 -0
  203. package/src/web/textarea/manifest.json +438 -0
  204. package/src/web/textarea/textarea.css +1 -0
  205. package/src/web/textarea/textarea.vue +458 -0
  206. package/src/web/textarea/textarea.vue.d.ts +265 -0
  207. package/src/web/tooltip/AI_GUIDE.md +110 -0
  208. package/src/web/tooltip/index.ts +3 -0
  209. package/src/web/tooltip/manifest.json +222 -0
  210. package/src/web/tooltip/tooltip.css +1 -0
  211. package/src/web/tooltip/tooltip.vue +417 -0
  212. package/src/web/tooltip/tooltip.vue.d.ts +150 -0
  213. package/src/webview/bottomsheet/AI_GUIDE.md +141 -0
  214. package/src/webview/bottomsheet/bottomsheet.css +1 -0
  215. package/src/webview/bottomsheet/bottomsheet.vue +441 -0
  216. package/src/webview/bottomsheet/bottomsheet.vue.d.ts +178 -0
  217. package/src/webview/bottomsheet/index.ts +4 -0
  218. package/src/webview/bottomsheet/manifest.json +377 -0
  219. package/src/webview/button/AI_GUIDE.md +79 -0
  220. package/src/webview/button/button-ios.css +1 -0
  221. package/src/webview/button/button-ios.vue +138 -0
  222. package/src/webview/button/button-ios.vue.d.ts +102 -0
  223. package/src/webview/button/button-material.css +1 -0
  224. package/src/webview/button/button-material.vue +138 -0
  225. package/src/webview/button/button-material.vue.d.ts +102 -0
  226. package/src/webview/button/index.ts +7 -0
  227. package/src/webview/button/manifest.json +148 -0
  228. package/src/webview/index.ts +7 -0
  229. package/tailwind-preset.cjs +62 -0
  230. package/tailwind-preset.js +62 -0
  231. package/ttdk-manifest.json +10188 -0
  232. package/README.md +0 -3
@@ -0,0 +1,1257 @@
1
+ <template>
2
+ <div
3
+ ref="rootRef"
4
+ :class="`ttdk-dropdown-wrapper ${className || ''}`.trim()"
5
+ :data-disabled="disabled"
6
+ :data-error="Boolean(error || errorMessage)"
7
+ :data-size="size || 'md'"
8
+ :data-block="block ? 'true' : undefined"
9
+ :style="{
10
+ ...buildUiVars(ui),
11
+ ...(ui || {}),
12
+ ...(typeof style === 'object' ? style : {}),
13
+ }"
14
+ >
15
+ <template v-if="label">
16
+ <label
17
+ :class="`ttdk-dropdown-label ${labelClass || ''}`.trim()"
18
+ :for="id"
19
+ :style="{
20
+ ...buildUiVars(labelStyle),
21
+ }"
22
+ ><span>{{ label }}</span>
23
+ <template v-if="required">
24
+ <span class="ttdk-dropdown-required" aria-hidden="true"> * </span>
25
+ </template>
26
+
27
+ <template v-if="optional">
28
+ <span class="ttdk-dropdown-optional">{{
29
+ optionalText || "(Opsional)"
30
+ }}</span>
31
+ </template>
32
+ </label>
33
+ </template>
34
+
35
+ <template v-if="helperText && helperTextPosition === 'top'">
36
+ <div
37
+ :class="`ttdk-dropdown-helper-text ${helperTextClass || ''}`.trim()"
38
+ :style="{
39
+ ...buildUiVars(helperTextStyle),
40
+ }"
41
+ >
42
+ {{ helperText }}
43
+ </div>
44
+ </template>
45
+
46
+ <div
47
+ role="button"
48
+ aria-haspopup="menu"
49
+ ref="controlRef"
50
+ :class="`ttdk-dropdown-control ${controlClass || ''}`.trim()"
51
+ :id="id"
52
+ :tabIndex="disabled ? -1 : 0"
53
+ :aria-expanded="isOpen"
54
+ :aria-disabled="disabled"
55
+ :aria-invalid="Boolean(error || errorMessage)"
56
+ :data-open="isOpen ? 'true' : 'false'"
57
+ :data-filled="hasValue() ? 'true' : 'false'"
58
+ :data-error="Boolean(error || errorMessage)"
59
+ :data-disabled="Boolean(disabled)"
60
+ :data-size="size || 'md'"
61
+ :data-rounded="rounded || 'md'"
62
+ :data-custom-trigger="$slots.default ? 'true' : undefined"
63
+ :style="{
64
+ ...buildUiVars(controlStyle),
65
+ }"
66
+ @click="async (event) => toggleDropdown()"
67
+ @keydown="
68
+ async (e) => {
69
+ if (e.key === 'Enter' || e.key === ' ') {
70
+ e.preventDefault();
71
+ toggleDropdown();
72
+ } else if (e.key === 'Escape') {
73
+ closeDropdown();
74
+ }
75
+ }
76
+ "
77
+ >
78
+ <template v-if="$slots.default">
79
+ <slot />
80
+ </template>
81
+
82
+ <template v-if="!$slots.default">
83
+ <div class="ttdk-dropdown-display">
84
+ <template v-if="icon">
85
+ <span class="ttdk-dropdown-leading-icon" aria-hidden="true">{{
86
+ icon
87
+ }}</span>
88
+ </template>
89
+
90
+ <template v-if="multiple && hasValue()">
91
+ <div class="ttdk-dropdown-chips">
92
+ <template
93
+ :key="chip.value"
94
+ v-for="(chip, index) in getMultiSelectedChips()"
95
+ >
96
+ <span class="ttdk-dropdown-chip"
97
+ ><span class="ttdk-dropdown-chip-text">{{ chip.label }}</span
98
+ ><button
99
+ type="button"
100
+ class="ttdk-dropdown-chip-remove"
101
+ :aria-label="`Hapus ${chip.label}`"
102
+ @click="async (e) => handleRemoveTag(chip.value, e)"
103
+ >
104
+ <svg
105
+ class="ttdk-dropdown-chip-remove-icon"
106
+ viewBox="0 0 24 24"
107
+ fill="none"
108
+ >
109
+ <line x1="18" y1="6" x2="6" y2="18"></line>
110
+ <line x1="6" y1="6" x2="18" y2="18"></line>
111
+ </svg></button
112
+ ></span>
113
+ </template>
114
+ </div>
115
+ </template>
116
+
117
+ <template v-if="!multiple && hasValue()">
118
+ <span class="ttdk-dropdown-value">{{
119
+ getSingleSelectedLabel()
120
+ }}</span>
121
+ </template>
122
+
123
+ <template v-if="!hasValue()">
124
+ <span class="ttdk-dropdown-placeholder">{{
125
+ placeholder || "Pilih dropdown menu"
126
+ }}</span>
127
+ </template>
128
+ </div>
129
+
130
+ <template
131
+ v-if="clearable && hasValue() && !disabled && !readOnly && !loading"
132
+ >
133
+ <button
134
+ type="button"
135
+ class="ttdk-dropdown-clear"
136
+ aria-label="Hapus pilihan"
137
+ @click="async (e) => handleClear(e)"
138
+ >
139
+ <svg
140
+ class="ttdk-dropdown-clear-icon"
141
+ viewBox="0 0 24 24"
142
+ fill="none"
143
+ >
144
+ <line x1="18" y1="6" x2="6" y2="18"></line>
145
+ <line x1="6" y1="6" x2="18" y2="18"></line>
146
+ </svg>
147
+ </button>
148
+ </template>
149
+
150
+ <template v-if="loading">
151
+ <span class="ttdk-dropdown-spinner" aria-hidden="true"
152
+ ><svg
153
+ class="ttdk-dropdown-spinner-icon"
154
+ viewBox="0 0 24 24"
155
+ fill="none"
156
+ >
157
+ <line
158
+ x1="12"
159
+ y1="2.5"
160
+ x2="12"
161
+ y2="6.5"
162
+ opacity="0.175"
163
+ transform="rotate(0 12 12)"
164
+ ></line>
165
+ <line
166
+ x1="12"
167
+ y1="2.5"
168
+ x2="12"
169
+ y2="6.5"
170
+ opacity="0.25"
171
+ transform="rotate(30 12 12)"
172
+ ></line>
173
+ <line
174
+ x1="12"
175
+ y1="2.5"
176
+ x2="12"
177
+ y2="6.5"
178
+ opacity="0.325"
179
+ transform="rotate(60 12 12)"
180
+ ></line>
181
+ <line
182
+ x1="12"
183
+ y1="2.5"
184
+ x2="12"
185
+ y2="6.5"
186
+ opacity="0.4"
187
+ transform="rotate(90 12 12)"
188
+ ></line>
189
+ <line
190
+ x1="12"
191
+ y1="2.5"
192
+ x2="12"
193
+ y2="6.5"
194
+ opacity="0.475"
195
+ transform="rotate(120 12 12)"
196
+ ></line>
197
+ <line
198
+ x1="12"
199
+ y1="2.5"
200
+ x2="12"
201
+ y2="6.5"
202
+ opacity="0.55"
203
+ transform="rotate(150 12 12)"
204
+ ></line>
205
+ <line
206
+ x1="12"
207
+ y1="2.5"
208
+ x2="12"
209
+ y2="6.5"
210
+ opacity="0.625"
211
+ transform="rotate(180 12 12)"
212
+ ></line>
213
+ <line
214
+ x1="12"
215
+ y1="2.5"
216
+ x2="12"
217
+ y2="6.5"
218
+ opacity="0.7"
219
+ transform="rotate(210 12 12)"
220
+ ></line>
221
+ <line
222
+ x1="12"
223
+ y1="2.5"
224
+ x2="12"
225
+ y2="6.5"
226
+ opacity="0.775"
227
+ transform="rotate(240 12 12)"
228
+ ></line>
229
+ <line
230
+ x1="12"
231
+ y1="2.5"
232
+ x2="12"
233
+ y2="6.5"
234
+ opacity="0.85"
235
+ transform="rotate(270 12 12)"
236
+ ></line>
237
+ <line
238
+ x1="12"
239
+ y1="2.5"
240
+ x2="12"
241
+ y2="6.5"
242
+ opacity="0.925"
243
+ transform="rotate(300 12 12)"
244
+ ></line>
245
+ <line
246
+ x1="12"
247
+ y1="2.5"
248
+ x2="12"
249
+ y2="6.5"
250
+ opacity="1"
251
+ transform="rotate(330 12 12)"
252
+ ></line></svg
253
+ ></span>
254
+ </template>
255
+
256
+ <template v-if="!loading && arrow !== false">
257
+ <span
258
+ class="ttdk-dropdown-arrow"
259
+ :data-open="isOpen ? 'true' : 'false'"
260
+ ><svg
261
+ class="ttdk-dropdown-arrow-icon"
262
+ viewBox="0 0 24 24"
263
+ fill="none"
264
+ >
265
+ <polyline points="6 9 12 15 18 9"></polyline></svg
266
+ ></span>
267
+ </template>
268
+ </template>
269
+ </div>
270
+ <template v-if="isOpen && !disabled">
271
+ <div
272
+ class="ttdk-dropdown-menu"
273
+ role="menu"
274
+ ref="dropdownRef"
275
+ :style="{
276
+ ...buildUiVars(listStyle),
277
+ ...(listStyle?.width !== undefined &&
278
+ listStyle?.width !== null &&
279
+ listStyle?.width !== ''
280
+ ? {
281
+ width:
282
+ typeof listStyle.width === 'number' ||
283
+ /^\d+(\.\d+)?$/.test(String(listStyle.width).trim())
284
+ ? `${String(listStyle.width).trim()}px`
285
+ : String(listStyle.width),
286
+ minWidth:
287
+ typeof listStyle.width === 'number' ||
288
+ /^\d+(\.\d+)?$/.test(String(listStyle.width).trim())
289
+ ? `${String(listStyle.width).trim()}px`
290
+ : String(listStyle.width),
291
+ }
292
+ : {}),
293
+ }"
294
+ >
295
+ <template v-if="searchable">
296
+ <div
297
+ class="ttdk-dropdown-search"
298
+ @click="async (e) => e.stopPropagation()"
299
+ >
300
+ <input
301
+ v-focus
302
+ type="text"
303
+ class="ttdk-dropdown-search-input"
304
+ :placeholder="searchPlaceholder || 'Cari'"
305
+ :value="searchQuery"
306
+ @input="async (e) => handleSearchInput(e)"
307
+ :autoFocus="true"
308
+ /><span class="ttdk-dropdown-search-icon" aria-hidden="true"
309
+ ><svg
310
+ class="ttdk-dropdown-search-svg"
311
+ viewBox="0 0 24 24"
312
+ fill="none"
313
+ >
314
+ <circle cx="11" cy="11" r="8"></circle>
315
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg
316
+ ></span>
317
+ </div>
318
+ </template>
319
+
320
+ <template v-if="panelContent || $slots.panelContent">
321
+ <div
322
+ class="ttdk-dropdown-panel-content"
323
+ @click="async (e) => e.stopPropagation()"
324
+ >
325
+ <slot name="panelContent" :close="() => closeDropdown()">
326
+ <template v-if="typeof panelContent === 'function'">
327
+ {{
328
+ panelContent({
329
+ close: () => closeDropdown(),
330
+ })
331
+ }}
332
+ </template>
333
+
334
+ <template v-else>
335
+ {{ panelContent }}
336
+ </template>
337
+ </slot>
338
+ </div>
339
+ </template>
340
+
341
+ <template v-if="!panelContent && !$slots.panelContent">
342
+ <div class="ttdk-dropdown-options-list" role="presentation">
343
+ <template
344
+ :key="opt.value"
345
+ v-for="(opt, index) in filteredOptions()"
346
+ >
347
+ <div>
348
+ <template v-if="opt.divider">
349
+ <div class="ttdk-dropdown-divider" role="separator"></div>
350
+ </template>
351
+
352
+ <div
353
+ :class="`ttdk-dropdown-option ${listClass || ''}`.trim()"
354
+ :role="multiple ? 'menuitemcheckbox' : 'menuitem'"
355
+ :aria-selected="isOptionSelected(opt.value)"
356
+ :aria-disabled="opt.disabled"
357
+ :data-selected="
358
+ isOptionSelected(opt.value) ? 'true' : 'false'
359
+ "
360
+ :data-disabled="opt.disabled ? 'true' : 'false'"
361
+ :data-danger="opt.danger ? 'true' : undefined"
362
+ @click="
363
+ async (e) => {
364
+ e.stopPropagation();
365
+ if (multiple) {
366
+ handleMultiToggle(opt);
367
+ } else {
368
+ handleSingleSelect(opt);
369
+ }
370
+ }
371
+ "
372
+ >
373
+ <template v-if="multiple">
374
+ <span
375
+ class="ttdk-dropdown-checkbox"
376
+ :data-checked="
377
+ isOptionSelected(opt.value) ? 'true' : 'false'
378
+ "
379
+ >
380
+ <template v-if="isOptionSelected(opt.value)">
381
+ <svg
382
+ class="ttdk-dropdown-checkbox-icon"
383
+ viewBox="0 0 24 24"
384
+ fill="none"
385
+ >
386
+ <polyline points="20 6 9 17 4 12"></polyline>
387
+ </svg>
388
+ </template>
389
+ </span>
390
+ </template>
391
+
392
+ <template v-if="opt.icon">
393
+ <span
394
+ class="ttdk-dropdown-option-icon"
395
+ aria-hidden="true"
396
+ >{{ opt.icon }}</span
397
+ >
398
+ </template>
399
+
400
+ <div class="ttdk-dropdown-option-content">
401
+ <span class="ttdk-dropdown-option-label">{{
402
+ opt.label
403
+ }}</span>
404
+ <template v-if="opt.description">
405
+ <span class="ttdk-dropdown-option-desc">{{
406
+ opt.description
407
+ }}</span>
408
+ </template>
409
+ </div>
410
+ <template v-if="!multiple && isOptionSelected(opt.value)">
411
+ <span class="ttdk-dropdown-check-icon" aria-hidden="true"
412
+ ><svg
413
+ class="ttdk-dropdown-check-svg"
414
+ viewBox="0 0 24 24"
415
+ fill="none"
416
+ >
417
+ <polyline points="20 6 9 17 4 12"></polyline></svg
418
+ ></span>
419
+ </template>
420
+ </div>
421
+ </div>
422
+ </template>
423
+ <template v-if="filteredOptions().length === 0">
424
+ <div class="ttdk-dropdown-empty">Tidak ada pilihan ditemukan</div>
425
+ </template>
426
+ </div>
427
+ </template>
428
+
429
+ <template v-if="multiple">
430
+ <div
431
+ class="ttdk-dropdown-actions"
432
+ @click="async (e) => e.stopPropagation()"
433
+ >
434
+ <button
435
+ type="button"
436
+ class="ttdk-dropdown-btn-reset"
437
+ @click="async (event) => handleReset()"
438
+ >
439
+ <svg
440
+ class="ttdk-dropdown-btn-reset-icon"
441
+ viewBox="0 0 24 24"
442
+ fill="none"
443
+ >
444
+ <path
445
+ d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
446
+ ></path>
447
+ <path d="M3 3v5h5"></path></svg
448
+ ><span>{{ resetText || "Reset" }}</span></button
449
+ ><button
450
+ type="button"
451
+ class="ttdk-dropdown-btn-apply"
452
+ @click="async (event) => handleApply()"
453
+ >
454
+ <span>{{ applyText || "Terapkan" }}</span
455
+ ><svg
456
+ class="ttdk-dropdown-btn-apply-icon"
457
+ viewBox="0 0 24 24"
458
+ fill="none"
459
+ >
460
+ <polyline points="20 6 9 17 4 12"></polyline>
461
+ </svg>
462
+ </button>
463
+ </div>
464
+ </template>
465
+ </div>
466
+ </template>
467
+
468
+ <template v-if="helperText && helperTextPosition !== 'top'">
469
+ <div
470
+ :class="`ttdk-dropdown-helper-text ${helperTextClass || ''}`.trim()"
471
+ :style="{
472
+ ...buildUiVars(helperTextStyle),
473
+ }"
474
+ >
475
+ {{ helperText }}
476
+ </div>
477
+ </template>
478
+
479
+ <template v-if="errorMessage || (typeof error === 'string' && error)">
480
+ <div
481
+ role="alert"
482
+ :class="`ttdk-dropdown-error-text ${errorMessageClass || ''}`.trim()"
483
+ :style="{
484
+ ...buildUiVars(errorMessageStyle),
485
+ }"
486
+ >
487
+ {{ errorMessage || (typeof error === "string" ? error : "") }}
488
+ </div>
489
+ </template>
490
+ </div>
491
+ </template>
492
+
493
+ <script setup lang="ts">
494
+ const vFocus = {
495
+ mounted: (el: HTMLElement) => {
496
+ el.focus();
497
+ },
498
+ };
499
+ import { onMounted, onUnmounted, ref, useSlots } from "vue";
500
+ const slots = useSlots();
501
+
502
+ import {
503
+ computePosition,
504
+ autoUpdate,
505
+ flip,
506
+ shift,
507
+ offset,
508
+ size as floatingSize,
509
+ } from "@floating-ui/dom";
510
+ import { buildUiVars, UiProps } from "../../core/types";
511
+
512
+ export interface DropdownOption {
513
+ /**
514
+ * Teks label tampilan pilihan dropdown menu.
515
+ */
516
+ label: string;
517
+
518
+ /**
519
+ * Nilai unik (value) dari item dropdown.
520
+ */
521
+ value: string | number;
522
+
523
+ /**
524
+ * Status apakah opsi ini dinonaktifkan dari pemilihan.
525
+ * @default false
526
+ */
527
+ disabled?: boolean;
528
+
529
+ /**
530
+ * Ikon kustom di samping label item pilihan.
531
+ */
532
+ icon?: any;
533
+
534
+ /**
535
+ * Deskripsi atau teks bantuan kecil di bawah label item pilihan.
536
+ */
537
+ description?: string;
538
+
539
+ /**
540
+ * Menandai item sebagai aksi berbahaya/destruktif (misal: "Hapus"), ditampilkan dengan warna aksen merah.
541
+ * @default false
542
+ */
543
+ danger?: boolean;
544
+
545
+ /**
546
+ * Menampilkan garis pemisah horizontal (divider) di atas item ini.
547
+ * @default false
548
+ */
549
+ divider?: boolean;
550
+
551
+ /**
552
+ * Callback event saat item ini diklik.
553
+ */
554
+ onClick?: (option?: DropdownOption) => void;
555
+ }
556
+ export type DropdownMenuItem = DropdownOption;
557
+ export type DropdownUiProps = UiProps;
558
+ export interface DropdownProps {
559
+ /**
560
+ * Teks label judul field yang ditampilkan di atas dropdown (14px Semibold, #141313).
561
+ */
562
+ label?: string;
563
+
564
+ /**
565
+ * Menandai field sebagai opsi opsional dengan menambahkan teks '(Opsional)' (12px Regular, #9698A2).
566
+ * @default false
567
+ */
568
+ optional?: boolean;
569
+
570
+ /**
571
+ * Kustomisasi teks keterangan opsional.
572
+ * @default '(Opsional)'
573
+ */
574
+ optionalText?: string;
575
+
576
+ /**
577
+ * Teks petunjuk saat belum ada opsi yang dipilih.
578
+ * @default 'Pilih dropdown menu'
579
+ */
580
+ placeholder?: string;
581
+
582
+ /**
583
+ * Nilai terpilih saat ini (single string/number atau array nilai untuk mode multiple).
584
+ */
585
+ value?: any;
586
+
587
+ /**
588
+ * Nilai awal tidak terkontrol (uncontrolled initial value).
589
+ */
590
+ defaultValue?: any;
591
+
592
+ /**
593
+ * Daftar pilihan opsi dropdown menu. Dapat berupa array of object `{ label, value }` atau array of string.
594
+ */
595
+ options?: Array<DropdownOption | string>;
596
+
597
+ /**
598
+ * Mode pemilihan jamak (multi-select) dengan tampilan chips/tag dan daftar checkbox.
599
+ * @default false
600
+ */
601
+ multiple?: boolean;
602
+
603
+ /**
604
+ * Mengaktifkan kotak pencarian (search input) di dalam menu dropdown.
605
+ * @default false
606
+ */
607
+ searchable?: boolean;
608
+
609
+ /**
610
+ * Placeholder pada kotak pencarian di dalam dropdown.
611
+ * @default 'Cari'
612
+ */
613
+ searchPlaceholder?: string;
614
+
615
+ /**
616
+ * Teks tombol terapkan pada footer multi-select.
617
+ * @default 'Terapkan'
618
+ */
619
+ applyText?: string;
620
+
621
+ /**
622
+ * Teks tombol reset pada footer multi-select.
623
+ * @default 'Reset'
624
+ */
625
+ resetText?: string;
626
+
627
+ /**
628
+ * Nama form field HTML.
629
+ */
630
+ name?: string;
631
+
632
+ /**
633
+ * Identifier unik elemen dropdown.
634
+ */
635
+ id?: string;
636
+
637
+ /**
638
+ * Status apakah dropdown sedang dalam kondisi error / tidak valid.
639
+ * @default false
640
+ */
641
+ error?: boolean;
642
+
643
+ /**
644
+ * Pesan teks penjelasan kesalahan validasi di bawah field.
645
+ */
646
+ errorMessage?: string;
647
+
648
+ /**
649
+ * Teks petunjuk atau keterangan bantuan di bawah field (12px Regular, #3C4245).
650
+ */
651
+ helperText?: string;
652
+
653
+ /**
654
+ * Posisi teks helper
655
+ */
656
+ helperTextPosition?: "top" | "bottom";
657
+
658
+ /**
659
+ * Skala ukuran dropdown.
660
+ * - `'sm'`: 36px
661
+ * - `'md'`: 48px (standar desain)
662
+ * - `'lg'`: 56px
663
+ * @default 'md'
664
+ */
665
+ size?: "sm" | "md" | "lg";
666
+
667
+ /**
668
+ * Bentuk sudut kelengkungan dropdown.
669
+ * - `'none'`: 0px
670
+ * - `'sm'`: 4px
671
+ * - `'md'`: 8px (standar desain)
672
+ * - `'lg'`: 12px
673
+ * - `'full'`: 9999px (kapsul)
674
+ * @default 'md'
675
+ */
676
+ rounded?: "none" | "sm" | "md" | "lg" | "full";
677
+
678
+ /**
679
+ * Posisi penempatan panel menu dropdown popover terhadap elemen trigger.
680
+ * @default 'bottom-start'
681
+ */
682
+ placement?:
683
+ | "bottom-start"
684
+ | "bottom-end"
685
+ | "bottom"
686
+ | "top-start"
687
+ | "top-end"
688
+ | "top";
689
+
690
+ /**
691
+ * Menampilkan panah chevron indikator buka/tutup pada trigger.
692
+ * @default true
693
+ */
694
+ arrow?: boolean;
695
+
696
+ /**
697
+ * Ikon kustom di awal teks trigger (leading icon).
698
+ */
699
+ icon?: any;
700
+
701
+ /**
702
+ * Otomatis menutup menu popover setelah pengguna memilih opsi (pada single select).
703
+ * @default true
704
+ */
705
+ closeOnSelect?: boolean;
706
+
707
+ /**
708
+ * Menjadikan lebar field dropdown memenuhi kontainer penuh (full-width 100%).
709
+ * @default false
710
+ */
711
+ block?: boolean;
712
+
713
+ /**
714
+ * Status apakah dropdown dinonaktifkan (disabled).
715
+ * @default false
716
+ */
717
+ disabled?: boolean;
718
+
719
+ /**
720
+ * Menandai field sebagai input wajib diisi (menampilkan tanda bintang merah *).
721
+ * @default false
722
+ */
723
+ required?: boolean;
724
+
725
+ /**
726
+ * Ref instance elemen control dropdown untuk akses DOM langsung.
727
+ */
728
+ inputRef?: any;
729
+
730
+ /**
731
+ * Menampilkan tombol × untuk menghapus pilihan yang sudah dipilih.
732
+ * @default false
733
+ */
734
+ clearable?: boolean;
735
+
736
+ /**
737
+ * Menampilkan spinner loading dan menonaktifkan interaksi dropdown.
738
+ * @default false
739
+ */
740
+ loading?: boolean;
741
+
742
+ /**
743
+ * Menjadikan dropdown hanya-baca (tidak bisa dibuka/diubah).
744
+ * @default false
745
+ */
746
+ readOnly?: boolean;
747
+
748
+ /**
749
+ * Kustomisasi visual granular wrapper utama (UiProps).
750
+ */
751
+ ui?: DropdownUiProps;
752
+
753
+ /**
754
+ * Style inline native untuk wrapper luar.
755
+ */
756
+ style?: any;
757
+
758
+ /**
759
+ * Style kustom untuk elemen label (UiProps).
760
+ */
761
+ labelStyle?: UiProps;
762
+
763
+ /**
764
+ * Class CSS kustom untuk elemen label (Tailwind / class kustom).
765
+ */
766
+ labelClass?: string;
767
+
768
+ /**
769
+ * Style kustom untuk elemen kotak pemicu (control trigger) (UiProps).
770
+ */
771
+ controlStyle?: UiProps;
772
+
773
+ /**
774
+ * Class CSS kustom untuk elemen kotak pemicu (control trigger) (Tailwind / class kustom).
775
+ */
776
+ controlClass?: string;
777
+
778
+ /**
779
+ * Style kustom untuk panel popover / menu list opsi (UiProps).
780
+ */
781
+ listStyle?: UiProps;
782
+
783
+ /**
784
+ * Class CSS kustom untuk panel popover / menu list opsi (Tailwind / class kustom).
785
+ */
786
+ listClass?: string;
787
+
788
+ /**
789
+ * Style kustom untuk pesan teks error validasi (UiProps).
790
+ */
791
+ errorMessageStyle?: UiProps;
792
+
793
+ /**
794
+ * Class CSS kustom untuk pesan teks error validasi (Tailwind / class kustom).
795
+ */
796
+ errorMessageClass?: string;
797
+
798
+ /**
799
+ * Style kustom untuk teks petunjuk bantuan (UiProps).
800
+ */
801
+ helperTextStyle?: UiProps;
802
+
803
+ /**
804
+ * Class CSS kustom untuk teks petunjuk bantuan (Tailwind / class kustom).
805
+ */
806
+ helperTextClass?: string;
807
+
808
+ /**
809
+ * Callback event saat nilai pilihan berubah.
810
+ */
811
+ onChange?: (value: any) => void;
812
+
813
+ /**
814
+ * Callback event saat sebuah opsi dipilih dari menu.
815
+ */
816
+ onSelect?: (option: DropdownOption) => void;
817
+
818
+ /**
819
+ * Callback event saat pengguna mengetik di kolom pencarian.
820
+ */
821
+ onSearch?: (query: string) => void;
822
+
823
+ /**
824
+ * Callback event saat menu dropdown terbuka.
825
+ */
826
+ onOpen?: () => void;
827
+
828
+ /**
829
+ * Callback event saat menu dropdown ditutup.
830
+ */
831
+ onClose?: () => void;
832
+
833
+ /**
834
+ * Menonaktifkan middleware `shift` pada floating-ui sehingga posisi list popover
835
+ * tidak bergeser secara horizontal mengikuti tepi layar.
836
+ * Berguna saat ingin list tetap berada di tengah atau posisi placement yang ditentukan.
837
+ * @default false
838
+ */
839
+ disableShift?: boolean;
840
+
841
+ /**
842
+ * Class CSS kustom wrapper luar.
843
+ */
844
+ className?: string;
845
+
846
+ /**
847
+ * Elemen pemicu (trigger) kustom, tombol, ikon, atau komponen anak lainnya.
848
+ */
849
+ children?: any;
850
+
851
+ /**
852
+ * Konten kustom yang ditampilkan di dalam panel popover menggantikan list opsi standar.
853
+ * Berguna untuk menampilkan komponen seperti Calendar, Color Picker, dsb. di dalam Dropdown.
854
+ * Ketika prop ini di-set, `options` tidak akan dirender.
855
+ * Menerima ReactNode atau render function `({ close }) => ReactNode`.
856
+ */
857
+ panelContent?: any;
858
+
859
+ /**
860
+ * Label atau teks kustom untuk ditampilkan pada trigger dropdown.
861
+ * Sangat berguna saat menggunakan `panelContent` seperti Date Picker di mana nilai trigger diformat secara kustom.
862
+ */
863
+ displayValue?: string;
864
+
865
+ /**
866
+ * Strategi positioning Floating UI ('fixed' atau 'absolute').
867
+ * Default 'fixed' agar dropdown tidak terpotong (clipped) oleh kontainer parent yang memiliki `overflow: hidden`.
868
+ * @default 'fixed'
869
+ */
870
+ // strategy?: 'fixed' | 'absolute';
871
+ }
872
+ export type DropdownMenuProps = DropdownProps;
873
+
874
+ /**
875
+ * Komponen Dropdown Menu TTDK sesuai spesifikasi Desain:
876
+ * - Selaras UI & Props dengan Selectbox: Label, Helper/Error text, Search bar, Floating popover
877
+ * - Mode Single Select / Menu Actions dengan Active Teal Checkmark (#00A3A6)
878
+ * - Mode Multi-Select dengan Checkbox list, Chip tags (Label ✕), dan Action footer (Reset & Terapkan)
879
+ * - Varian trigger fleksibel: 'select' (standar form), 'action' / 'primary' (tombol aksi), 'outline', 'ghost'
880
+ * - Dukungan Trigger Kustom melalui Slot / Children (misal: tombol kustom)
881
+ * - Dukungan item kaya: Icon, Deskripsi, Divider pemisah seksi, dan Danger item
882
+ */
883
+
884
+ const props = withDefaults(defineProps<DropdownProps>(), {
885
+ arrow: true,
886
+ closeOnSelect: true,
887
+ });
888
+ const isOpen = ref(false);
889
+ const searchQuery = ref("");
890
+ const currentValue = ref(
891
+ props.value !== undefined
892
+ ? props.value
893
+ : props.defaultValue !== undefined
894
+ ? props.defaultValue
895
+ : props.multiple
896
+ ? []
897
+ : ""
898
+ );
899
+ const tempMultiValue = ref([]);
900
+ const cleanupAutoUpdate = ref(null);
901
+
902
+ const rootRef = ref<HTMLDivElement>(null);
903
+ const controlRef = ref<HTMLDivElement>(null);
904
+ const dropdownRef = ref<HTMLDivElement>(null);
905
+
906
+ onMounted(() => {
907
+ if (typeof document !== "undefined") {
908
+ document.addEventListener(
909
+ "pointerdown",
910
+ (e: any) => onPointerDown(e),
911
+ true
912
+ );
913
+ }
914
+ });
915
+ onUnmounted(() => {
916
+ stopFloating();
917
+ if (typeof document !== "undefined") {
918
+ document.removeEventListener(
919
+ "pointerdown",
920
+ (e: any) => onPointerDown(e),
921
+ true
922
+ );
923
+ }
924
+ });
925
+
926
+ function updatePosition() {
927
+ if (!controlRef.value || !dropdownRef.value) return;
928
+ dropdownRef.value.style.maxHeight = "";
929
+ computePosition(controlRef.value, dropdownRef.value, {
930
+ placement: props.placement || "bottom-start",
931
+ strategy: "absolute",
932
+ middleware: [
933
+ offset(4),
934
+ flip({
935
+ fallbackPlacements: [
936
+ "top-start",
937
+ "top-end",
938
+ "bottom-start",
939
+ "bottom-end",
940
+ ],
941
+ padding: {
942
+ top: 16,
943
+ bottom: 16,
944
+ left: 16,
945
+ right: 16,
946
+ },
947
+ }),
948
+ ...(props.disableShift
949
+ ? []
950
+ : [
951
+ shift({
952
+ padding: {
953
+ top: 16,
954
+ bottom: 16,
955
+ left: 16,
956
+ right: 16,
957
+ },
958
+ }),
959
+ ]),
960
+ floatingSize({
961
+ apply({ rects, elements, availableHeight }) {
962
+ const isPanel = Boolean(props.panelContent || slots.panelContent);
963
+ const customWidth =
964
+ props.listStyle?.width !== undefined &&
965
+ props.listStyle?.width !== null &&
966
+ props.listStyle?.width !== ""
967
+ ? typeof props.listStyle.width === "number" ||
968
+ /^\d+(\.\d+)?$/.test(String(props.listStyle.width).trim())
969
+ ? `${String(props.listStyle.width).trim()}px`
970
+ : String(props.listStyle.width)
971
+ : undefined;
972
+ const customMaxHeight =
973
+ props.listStyle?.maxHeight !== undefined &&
974
+ props.listStyle?.maxHeight !== null &&
975
+ props.listStyle?.maxHeight !== ""
976
+ ? props.listStyle.maxHeight === "none"
977
+ ? "none"
978
+ : typeof props.listStyle.maxHeight === "number" ||
979
+ /^\d+(\.\d+)?$/.test(String(props.listStyle.maxHeight).trim())
980
+ ? `${String(props.listStyle.maxHeight).trim()}px`
981
+ : String(props.listStyle.maxHeight)
982
+ : undefined;
983
+ Object.assign(elements.floating.style, {
984
+ width:
985
+ customWidth || (isPanel ? "auto" : `${rects.reference.width}px`),
986
+ minWidth: customWidth ? customWidth : isPanel ? "auto" : undefined,
987
+ maxHeight:
988
+ customMaxHeight !== undefined
989
+ ? customMaxHeight
990
+ : isPanel
991
+ ? "none"
992
+ : `${Math.max(180, Math.min(availableHeight, 360))}px`,
993
+ });
994
+ },
995
+ padding: {
996
+ top: 16,
997
+ bottom: 16,
998
+ left: 16,
999
+ right: 16,
1000
+ },
1001
+ }),
1002
+ ],
1003
+ }).then(({ x, y, placement }: any) => {
1004
+ if (!dropdownRef.value) return;
1005
+ dropdownRef.value.style.position = "absolute";
1006
+ dropdownRef.value.style.left = `${x}px`;
1007
+ dropdownRef.value.style.top = `${y}px`;
1008
+ dropdownRef.value.setAttribute("data-placement", placement);
1009
+ });
1010
+ }
1011
+ function startFloating() {
1012
+ stopFloating();
1013
+ if (controlRef.value && dropdownRef.value) {
1014
+ cleanupAutoUpdate.value = autoUpdate(
1015
+ controlRef.value,
1016
+ dropdownRef.value,
1017
+ () => {
1018
+ updatePosition();
1019
+ }
1020
+ );
1021
+ }
1022
+ }
1023
+ function stopFloating() {
1024
+ if (typeof cleanupAutoUpdate.value === "function") {
1025
+ cleanupAutoUpdate.value();
1026
+ cleanupAutoUpdate.value = null;
1027
+ }
1028
+ }
1029
+ function handleClickOutside(event: any) {
1030
+ if (!dropdownRef.value) return;
1031
+ const target = event.target;
1032
+ if (
1033
+ rootRef.value &&
1034
+ !rootRef.value.contains(target) &&
1035
+ dropdownRef.value &&
1036
+ !dropdownRef.value.contains(target)
1037
+ ) {
1038
+ closeDropdown();
1039
+ }
1040
+ }
1041
+ function onPointerDown(event: any) {
1042
+ handleClickOutside(event);
1043
+ }
1044
+ function toggleDropdown() {
1045
+ if (props.disabled || props.readOnly || props.loading) return;
1046
+ if (isOpen.value) {
1047
+ closeDropdown();
1048
+ } else {
1049
+ openDropdown();
1050
+ }
1051
+ }
1052
+ function openDropdown() {
1053
+ if (props.disabled || props.readOnly || props.loading) return;
1054
+ isOpen.value = true;
1055
+ searchQuery.value = "";
1056
+ if (props.multiple) {
1057
+ tempMultiValue.value = Array.isArray(currentValue.value)
1058
+ ? [...currentValue.value]
1059
+ : [];
1060
+ }
1061
+ setTimeout(() => {
1062
+ startFloating();
1063
+ updatePosition();
1064
+ if (props.searchable && dropdownRef.value) {
1065
+ const searchInput = (dropdownRef.value as any).querySelector?.(
1066
+ ".ttdk-dropdown-search-input"
1067
+ );
1068
+ if (searchInput) {
1069
+ searchInput.focus();
1070
+ }
1071
+ }
1072
+ }, 0);
1073
+ if (props.onOpen) props.onOpen();
1074
+ }
1075
+ function closeDropdown() {
1076
+ stopFloating();
1077
+ isOpen.value = false;
1078
+ searchQuery.value = "";
1079
+ if (props.onClose) props.onClose();
1080
+ }
1081
+ function normalizedOptions() {
1082
+ const raw = props.options;
1083
+ if (!raw || !Array.isArray(raw)) return [];
1084
+ return raw.map((opt: any) => {
1085
+ if (typeof opt === "object" && opt !== null) {
1086
+ return {
1087
+ label: String(opt.label ?? opt.value ?? ""),
1088
+ value: opt.value ?? opt.label ?? "",
1089
+ disabled: Boolean(opt.disabled),
1090
+ icon: opt.icon,
1091
+ description: opt.description,
1092
+ danger: Boolean(opt.danger),
1093
+ divider: Boolean(opt.divider),
1094
+ onClick: opt.onClick,
1095
+ };
1096
+ }
1097
+ return {
1098
+ label: String(opt),
1099
+ value: opt,
1100
+ disabled: false,
1101
+ icon: undefined,
1102
+ description: undefined,
1103
+ danger: false,
1104
+ divider: false,
1105
+ onClick: undefined,
1106
+ };
1107
+ });
1108
+ }
1109
+ function filteredOptions() {
1110
+ const opts = normalizedOptions();
1111
+ if (!searchQuery.value || !searchQuery.value.trim()) return opts;
1112
+ const q = searchQuery.value.toLowerCase().trim();
1113
+ return opts.filter((opt: any) => {
1114
+ const matchLabel = opt.label && opt.label.toLowerCase().includes(q);
1115
+ const matchDesc =
1116
+ opt.description && opt.description.toLowerCase().includes(q);
1117
+ return matchLabel || matchDesc;
1118
+ });
1119
+ }
1120
+ function isOptionSelected(optValue: any) {
1121
+ if (props.multiple) {
1122
+ const list = isOpen.value
1123
+ ? tempMultiValue.value
1124
+ : Array.isArray(currentValue.value)
1125
+ ? currentValue.value
1126
+ : [];
1127
+ return list.some((v: any) => String(v) === String(optValue));
1128
+ }
1129
+ return String(currentValue.value) === String(optValue);
1130
+ }
1131
+ function handleSingleSelect(opt: any) {
1132
+ if (opt.disabled || props.disabled) return;
1133
+ if (opt.onClick) {
1134
+ opt.onClick(opt);
1135
+ }
1136
+ currentValue.value = opt.value;
1137
+ if (props.onSelect) {
1138
+ props.onSelect(opt);
1139
+ }
1140
+ if (props.onChange) {
1141
+ props.onChange(opt.value);
1142
+ }
1143
+ if (props.closeOnSelect !== false) {
1144
+ closeDropdown();
1145
+ }
1146
+ }
1147
+ function handleMultiToggle(opt: any) {
1148
+ if (opt.disabled || props.disabled) return;
1149
+ if (opt.onClick) {
1150
+ opt.onClick(opt);
1151
+ }
1152
+ const index = tempMultiValue.value.findIndex(
1153
+ (v: any) => String(v) === String(opt.value)
1154
+ );
1155
+ if (index > -1) {
1156
+ tempMultiValue.value.splice(index, 1);
1157
+ } else {
1158
+ tempMultiValue.value.push(opt.value);
1159
+ }
1160
+ }
1161
+ function handleRemoveTag(val: any, event: any) {
1162
+ if (event && event.stopPropagation) event.stopPropagation();
1163
+ if (props.disabled) return;
1164
+ if (Array.isArray(currentValue.value)) {
1165
+ const next = currentValue.value.filter(
1166
+ (v: any) => String(v) !== String(val)
1167
+ );
1168
+ currentValue.value = next;
1169
+ tempMultiValue.value = [...next];
1170
+ if (props.onChange) {
1171
+ props.onChange(next);
1172
+ }
1173
+ }
1174
+ }
1175
+ function handleApply() {
1176
+ currentValue.value = [...tempMultiValue.value];
1177
+ if (props.onChange) {
1178
+ props.onChange(currentValue.value);
1179
+ }
1180
+ closeDropdown();
1181
+ }
1182
+ function handleReset() {
1183
+ tempMultiValue.value = [];
1184
+ currentValue.value = [];
1185
+ if (props.onChange) {
1186
+ props.onChange([]);
1187
+ }
1188
+ }
1189
+ function handleSearchInput(e: any) {
1190
+ searchQuery.value = e.target.value;
1191
+ if (props.onSearch) {
1192
+ props.onSearch(e.target.value);
1193
+ }
1194
+ }
1195
+ function getSingleSelectedLabel() {
1196
+ if (
1197
+ props.displayValue !== undefined &&
1198
+ props.displayValue !== null &&
1199
+ props.displayValue !== ""
1200
+ ) {
1201
+ return props.displayValue;
1202
+ }
1203
+ const opts = normalizedOptions();
1204
+ const match = opts.find(
1205
+ (opt: any) => String(opt.value) === String(currentValue.value)
1206
+ );
1207
+ if (match) {
1208
+ return match.label;
1209
+ }
1210
+ if (
1211
+ currentValue.value !== undefined &&
1212
+ currentValue.value !== null &&
1213
+ currentValue.value !== ""
1214
+ ) {
1215
+ return String(currentValue.value);
1216
+ }
1217
+ return "";
1218
+ }
1219
+ function getMultiSelectedChips() {
1220
+ const list = Array.isArray(currentValue.value) ? currentValue.value : [];
1221
+ const opts = normalizedOptions();
1222
+ return list.map((val: any) => {
1223
+ const match = opts.find((o: any) => String(o.value) === String(val));
1224
+ return {
1225
+ value: val,
1226
+ label: match ? match.label : String(val),
1227
+ };
1228
+ });
1229
+ }
1230
+ function hasValue() {
1231
+ if (
1232
+ props.displayValue !== undefined &&
1233
+ props.displayValue !== null &&
1234
+ props.displayValue !== ""
1235
+ ) {
1236
+ return true;
1237
+ }
1238
+ if (props.multiple) {
1239
+ return Array.isArray(currentValue.value) && currentValue.value.length > 0;
1240
+ }
1241
+ return (
1242
+ currentValue.value !== undefined &&
1243
+ currentValue.value !== "" &&
1244
+ currentValue.value !== null
1245
+ );
1246
+ }
1247
+ function handleClear(event: any) {
1248
+ if (event && event.stopPropagation) event.stopPropagation();
1249
+ if (props.disabled || props.readOnly) return;
1250
+ const emptyVal = props.multiple ? [] : "";
1251
+ currentValue.value = emptyVal;
1252
+ tempMultiValue.value = [];
1253
+ if (props.onChange) {
1254
+ props.onChange(emptyVal);
1255
+ }
1256
+ }
1257
+ </script>