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