@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,1536 @@
1
+ <template>
2
+ <div
3
+ role="group"
4
+ aria-label="Kalender"
5
+ :id="id"
6
+ :class="
7
+ `ttdk-calendar ttdk-calendar-${variant || 'bordered'} ttdk-calendar-${
8
+ size || 'md'
9
+ } ttdk-calendar-rounded-${rounded || 'md'} ${
10
+ disabled ? 'ttdk-calendar-disabled' : ''
11
+ } ${className || ''}`.trim()
12
+ "
13
+ :style="{
14
+ ...buildUiVars(ui),
15
+ ...(typeof style === 'object' ? style : {}),
16
+ }"
17
+ >
18
+ <template v-if="getViewMode() === 'day'">
19
+ <div class="ttdk-calendar-months">
20
+ <template
21
+ :key="`${monthItem.year}-${monthItem.month}`"
22
+ v-for="(monthItem, index) in getMonths()"
23
+ >
24
+ <div class="ttdk-calendar-month">
25
+ <div
26
+ :class="`ttdk-calendar-header ${headerClass || ''}`.trim()"
27
+ :style="{
28
+ ...buildUiStyle(headerStyle),
29
+ }"
30
+ >
31
+ <div class="ttdk-calendar-nav-group">
32
+ <template v-if="monthItem.isFirst">
33
+ <template v-if="yearControls !== false">
34
+ <button
35
+ type="button"
36
+ aria-label="Tahun Sebelumnya"
37
+ :class="
38
+ `ttdk-calendar-nav-btn ${navBtnClass || ''}`.trim()
39
+ "
40
+ :style="{
41
+ ...buildUiStyle(navBtnStyle),
42
+ }"
43
+ :disabled="disabled"
44
+ @click="async (event) => prevYear()"
45
+ >
46
+ <svg
47
+ viewBox="0 0 24 24"
48
+ fill="none"
49
+ stroke="currentColor"
50
+ >
51
+ <polyline points="11 17 6 12 11 7"></polyline>
52
+ <polyline points="18 17 13 12 18 7"></polyline>
53
+ </svg>
54
+ </button>
55
+ </template>
56
+
57
+ <button
58
+ type="button"
59
+ aria-label="Bulan Sebelumnya"
60
+ :class="`ttdk-calendar-nav-btn ${navBtnClass || ''}`.trim()"
61
+ :style="{
62
+ ...buildUiStyle(navBtnStyle),
63
+ }"
64
+ :disabled="disabled"
65
+ @click="async (event) => prevMonth()"
66
+ >
67
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
68
+ <polyline points="15 18 9 12 15 6"></polyline>
69
+ </svg>
70
+ </button>
71
+ </template>
72
+ </div>
73
+ <div class="ttdk-calendar-title-group">
74
+ <button
75
+ type="button"
76
+ class="ttdk-calendar-title-btn"
77
+ aria-label="Pilih Bulan"
78
+ :disabled="disabled"
79
+ @click="async (event) => openMonthView()"
80
+ >
81
+ {{ monthItem.monthName }}</button
82
+ ><button
83
+ type="button"
84
+ class="ttdk-calendar-title-btn"
85
+ aria-label="Pilih Tahun"
86
+ :disabled="disabled"
87
+ @click="async (event) => openYearView()"
88
+ >
89
+ {{ monthItem.year }}
90
+ </button>
91
+ </div>
92
+ <div class="ttdk-calendar-nav-group">
93
+ <template v-if="monthItem.isLast">
94
+ <button
95
+ type="button"
96
+ aria-label="Bulan Berikutnya"
97
+ :class="`ttdk-calendar-nav-btn ${navBtnClass || ''}`.trim()"
98
+ :style="{
99
+ ...buildUiStyle(navBtnStyle),
100
+ }"
101
+ :disabled="disabled"
102
+ @click="async (event) => nextMonth()"
103
+ >
104
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
105
+ <polyline points="9 18 15 12 9 6"></polyline>
106
+ </svg>
107
+ </button>
108
+
109
+ <template v-if="yearControls !== false">
110
+ <button
111
+ type="button"
112
+ aria-label="Tahun Berikutnya"
113
+ :class="
114
+ `ttdk-calendar-nav-btn ${navBtnClass || ''}`.trim()
115
+ "
116
+ :style="{
117
+ ...buildUiStyle(navBtnStyle),
118
+ }"
119
+ :disabled="disabled"
120
+ @click="async (event) => nextYear()"
121
+ >
122
+ <svg
123
+ viewBox="0 0 24 24"
124
+ fill="none"
125
+ stroke="currentColor"
126
+ >
127
+ <polyline points="13 17 18 12 13 7"></polyline>
128
+ <polyline points="6 17 11 12 6 7"></polyline>
129
+ </svg>
130
+ </button>
131
+ </template>
132
+ </template>
133
+ </div>
134
+ </div>
135
+ <table class="ttdk-calendar-table" role="grid">
136
+ <thead>
137
+ <tr class="ttdk-calendar-thead-row" role="row">
138
+ <template
139
+ :key="weekday"
140
+ v-for="(weekday, index) in getWeekDays()"
141
+ >
142
+ <th
143
+ class="ttdk-calendar-head-cell"
144
+ role="columnheader"
145
+ :aria-label="weekday"
146
+ >
147
+ {{ weekday }}
148
+ </th>
149
+ </template>
150
+ </tr>
151
+ </thead>
152
+ <tbody class="ttdk-calendar-tbody" role="rowgroup">
153
+ <template :key="wIdx" v-for="(week, wIdx) in monthItem.weeks">
154
+ <tr class="ttdk-calendar-row" role="row">
155
+ <template
156
+ :key="`${cell.dateStr}-${cIdx}`"
157
+ v-for="(cell, cIdx) in week"
158
+ >
159
+ <td
160
+ role="gridcell"
161
+ :class="
162
+ `ttdk-calendar-cell ${cell.rangeClass} ${
163
+ cellClass || ''
164
+ }`.trim()
165
+ "
166
+ :aria-selected="cell.selected ? 'true' : 'false'"
167
+ :aria-disabled="cell.disabled ? 'true' : 'false'"
168
+ :style="{
169
+ ...buildUiStyle(cellStyle),
170
+ }"
171
+ @mouseenter="
172
+ async (event) => handleDayHover(cell.dateStr)
173
+ "
174
+ @mouseleave="async (event) => handleDayHoverLeave()"
175
+ >
176
+ <button
177
+ type="button"
178
+ :class="`ttdk-calendar-day ${
179
+ cell.selected ? 'ttdk-calendar-day-selected' : ''
180
+ } ${cell.today ? 'ttdk-calendar-day-today' : ''} ${
181
+ cell.inRange ? 'ttdk-calendar-day-in-range' : ''
182
+ } ${
183
+ cell.isRangeStart
184
+ ? 'ttdk-calendar-day-range-start'
185
+ : ''
186
+ } ${
187
+ cell.isRangeEnd ? 'ttdk-calendar-day-range-end' : ''
188
+ } ${
189
+ cell.isOutside ? 'ttdk-calendar-day-outside' : ''
190
+ } ${
191
+ cell.disabled ? 'ttdk-calendar-day-disabled' : ''
192
+ } ${
193
+ cell.unavailable
194
+ ? 'ttdk-calendar-day-unavailable'
195
+ : ''
196
+ }`"
197
+ :disabled="cell.disabled"
198
+ :tabIndex="cell.selected || cell.today ? 0 : -1"
199
+ @click="
200
+ async (event) => handleSelectDate(cell.dateStr)
201
+ "
202
+ >
203
+ <span>{{ cell.day }}</span>
204
+ <template v-if="cell.marker">
205
+ <span
206
+ class="ttdk-calendar-marker-dot"
207
+ :style="{
208
+ backgroundColor: cell.marker
209
+ ? cell.marker.color
210
+ : undefined,
211
+ }"
212
+ :title="
213
+ cell.marker ? cell.marker.tooltip : undefined
214
+ "
215
+ ></span>
216
+ </template>
217
+ </button>
218
+ </td>
219
+ </template>
220
+ </tr>
221
+ </template>
222
+ </tbody>
223
+ </table>
224
+ </div>
225
+ </template>
226
+ </div>
227
+ </template>
228
+
229
+ <template v-if="getViewMode() === 'month'">
230
+ <div class="ttdk-calendar-month-picker-view">
231
+ <div
232
+ :class="`ttdk-calendar-header ${headerClass || ''}`.trim()"
233
+ :style="{
234
+ ...buildUiStyle(headerStyle),
235
+ }"
236
+ >
237
+ <div class="ttdk-calendar-nav-group">
238
+ <button
239
+ type="button"
240
+ aria-label="Tahun Sebelumnya"
241
+ :class="`ttdk-calendar-nav-btn ${navBtnClass || ''}`.trim()"
242
+ :style="{
243
+ ...buildUiStyle(navBtnStyle),
244
+ }"
245
+ :disabled="disabled"
246
+ @click="async (event) => prevYear()"
247
+ >
248
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
249
+ <polyline points="15 18 9 12 15 6"></polyline>
250
+ </svg>
251
+ </button>
252
+ </div>
253
+ <div class="ttdk-calendar-title-group">
254
+ <button
255
+ type="button"
256
+ class="ttdk-calendar-title-btn"
257
+ aria-label="Pilih Tahun"
258
+ :disabled="disabled"
259
+ @click="async (event) => openYearView()"
260
+ >
261
+ {{ getViewYear() }}
262
+ </button>
263
+ </div>
264
+ <div class="ttdk-calendar-nav-group">
265
+ <button
266
+ type="button"
267
+ aria-label="Tahun Berikutnya"
268
+ :class="`ttdk-calendar-nav-btn ${navBtnClass || ''}`.trim()"
269
+ :style="{
270
+ ...buildUiStyle(navBtnStyle),
271
+ }"
272
+ :disabled="disabled"
273
+ @click="async (event) => nextYear()"
274
+ >
275
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
276
+ <polyline points="9 18 15 12 9 6"></polyline>
277
+ </svg>
278
+ </button>
279
+ </div>
280
+ </div>
281
+ <div class="ttdk-calendar-grid-picker" role="grid">
282
+ <template :key="m.month" v-for="(m, index) in getMonthsList()">
283
+ <button
284
+ type="button"
285
+ :class="`ttdk-calendar-picker-item ${
286
+ m.isSelected ? 'ttdk-calendar-picker-item-selected' : ''
287
+ } ${m.isCurrent ? 'ttdk-calendar-picker-item-current' : ''}`"
288
+ :disabled="disabled"
289
+ :aria-label="m.fullName"
290
+ @click="async (event) => selectMonth(m.month)"
291
+ >
292
+ {{ m.label }}
293
+ </button>
294
+ </template>
295
+ </div>
296
+ </div>
297
+ </template>
298
+
299
+ <template v-if="getViewMode() === 'year'">
300
+ <div class="ttdk-calendar-year-picker-view">
301
+ <div
302
+ :class="`ttdk-calendar-header ${headerClass || ''}`.trim()"
303
+ :style="{
304
+ ...buildUiStyle(headerStyle),
305
+ }"
306
+ >
307
+ <div class="ttdk-calendar-nav-group">
308
+ <button
309
+ type="button"
310
+ aria-label="Rentang Tahun Sebelumnya"
311
+ :class="`ttdk-calendar-nav-btn ${navBtnClass || ''}`.trim()"
312
+ :style="{
313
+ ...buildUiStyle(navBtnStyle),
314
+ }"
315
+ :disabled="disabled"
316
+ @click="async (event) => prevDecade()"
317
+ >
318
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
319
+ <polyline points="15 18 9 12 15 6"></polyline>
320
+ </svg>
321
+ </button>
322
+ </div>
323
+ <div class="ttdk-calendar-title-group">
324
+ <span class="ttdk-calendar-title">{{ getDecadeLabel() }}</span>
325
+ </div>
326
+ <div class="ttdk-calendar-nav-group">
327
+ <button
328
+ type="button"
329
+ aria-label="Rentang Tahun Berikutnya"
330
+ :class="`ttdk-calendar-nav-btn ${navBtnClass || ''}`.trim()"
331
+ :style="{
332
+ ...buildUiStyle(navBtnStyle),
333
+ }"
334
+ :disabled="disabled"
335
+ @click="async (event) => nextDecade()"
336
+ >
337
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
338
+ <polyline points="9 18 15 12 9 6"></polyline>
339
+ </svg>
340
+ </button>
341
+ </div>
342
+ </div>
343
+ <div class="ttdk-calendar-grid-picker" role="grid">
344
+ <template :key="y.year" v-for="(y, index) in getYearsList()">
345
+ <button
346
+ type="button"
347
+ :class="`ttdk-calendar-picker-item ${
348
+ y.isSelected ? 'ttdk-calendar-picker-item-selected' : ''
349
+ } ${y.isCurrent ? 'ttdk-calendar-picker-item-current' : ''}`"
350
+ :disabled="disabled"
351
+ :aria-label="y.label"
352
+ @click="async (event) => selectYear(y.year)"
353
+ >
354
+ {{ y.label }}
355
+ </button>
356
+ </template>
357
+ </div>
358
+ </div>
359
+ </template>
360
+ </div>
361
+ </template>
362
+
363
+ <script setup lang="ts">
364
+ import { onUpdated, ref } from "vue";
365
+
366
+ import { buildUiStyle, buildUiVars, UiProps } from "../../core/types";
367
+
368
+ export type CalendarUiProps = UiProps;
369
+ export interface DateRange {
370
+ start: string | Date | null;
371
+ end: string | Date | null;
372
+ }
373
+ export interface DateMarker {
374
+ date: string;
375
+ color?: string;
376
+ tooltip?: string;
377
+ dot?: boolean;
378
+ }
379
+ export interface CalendarProps {
380
+ /**
381
+ * Nilai tanggal terpilih pada mode controlled.
382
+ * - Single: string ('YYYY-MM-DD') atau Date
383
+ * - Range: { start: string | Date | null, end: string | Date | null }
384
+ * - Multiple: string[] atau Date[]
385
+ */
386
+ value?: any;
387
+
388
+ /**
389
+ * Nilai awal tanggal terpilih pada mode uncontrolled.
390
+ */
391
+ defaultValue?: any;
392
+
393
+ /**
394
+ * Atribut ID unik untuk elemen root kalender.
395
+ */
396
+ id?: string;
397
+
398
+ /**
399
+ * Mengaktifkan mode pemilihan rentang tanggal (start date - end date).
400
+ * @default false
401
+ */
402
+ range?: boolean;
403
+
404
+ /**
405
+ * Mengaktifkan mode pemilihan banyak tanggal sekaligus.
406
+ * @default false
407
+ */
408
+ multiple?: boolean;
409
+
410
+ /**
411
+ * Batas tanggal paling awal yang dapat dipilih.
412
+ */
413
+ min?: string | Date;
414
+
415
+ /**
416
+ * Alias untuk min (kompatibel Nuxt UI: min-value).
417
+ */
418
+ minValue?: string | Date;
419
+
420
+ /**
421
+ * Batas tanggal paling akhir yang dapat dipilih.
422
+ */
423
+ max?: string | Date;
424
+
425
+ /**
426
+ * Alias untuk max (kompatibel Nuxt UI: max-value).
427
+ */
428
+ maxValue?: string | Date;
429
+
430
+ /**
431
+ * Fungsi untuk menonaktifkan tanggal tertentu secara spesifik (misal: hari libur / akhir pekan).
432
+ */
433
+ isDateDisabled?: (date: Date) => boolean;
434
+
435
+ /**
436
+ * Fungsi untuk menandai tanggal tidak tersedia (ditampilkan coret strikethrough).
437
+ */
438
+ isDateUnavailable?: (date: Date) => boolean;
439
+
440
+ /**
441
+ * Jumlah bulan yang ditampilkan secara berdampingan.
442
+ * @default 1
443
+ */
444
+ numberOfMonths?: number;
445
+
446
+ /**
447
+ * Hari awal dalam seminggu:
448
+ * - 0 = Minggu (Sunday)
449
+ * - 1 = Senin (Monday)
450
+ * @default 1
451
+ */
452
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
453
+
454
+ /**
455
+ * Kode lokal bahasa kalender (misal: 'id-ID', 'en-US').
456
+ * @default 'id-ID'
457
+ */
458
+ locale?: string;
459
+
460
+ /**
461
+ * Bulan yang sedang ditampilkan (1 - 12) pada mode navigasi controlled.
462
+ */
463
+ month?: number;
464
+
465
+ /**
466
+ * Tahun yang sedang ditampilkan (misal: 2026) pada mode navigasi controlled.
467
+ */
468
+ year?: number;
469
+
470
+ /**
471
+ * Bulan awal yang sedang ditampilkan (1 - 12).
472
+ */
473
+ defaultMonth?: number;
474
+
475
+ /**
476
+ * Tahun awal yang sedang ditampilkan.
477
+ */
478
+ defaultYear?: number;
479
+
480
+ /**
481
+ * Menampilkan tombol navigasi lompat tahun (<< dan >>).
482
+ * @default true
483
+ */
484
+ yearControls?: boolean;
485
+
486
+ /**
487
+ * Menampilkan hari-hari redup di luar bulan yang aktif (leading & trailing days).
488
+ * @default true
489
+ */
490
+ showOutsideDays?: boolean;
491
+
492
+ /**
493
+ * Selalu merender 6 baris minggu agar tinggi kalender konsisten stabil antar bulan.
494
+ * @default true
495
+ */
496
+ fixedWeeks?: boolean;
497
+
498
+ /**
499
+ * Daftar penanda agenda / jadwal faskes pada tanggal tertentu.
500
+ */
501
+ dateMarkers?: DateMarker[];
502
+
503
+ /**
504
+ * Skala ukuran kalender (sm: kompak 32px, md: standar 38px, lg: besar 44px).
505
+ * @default 'md'
506
+ */
507
+ size?: "sm" | "md" | "lg";
508
+
509
+ /**
510
+ * Radius sudut kelengkungan container kalender.
511
+ * @default 'md'
512
+ */
513
+ rounded?: "none" | "sm" | "md" | "lg" | "xl" | "full";
514
+
515
+ /**
516
+ * Varian visual tampilan kalender:
517
+ * - 'default': Bersih dengan background putih/gelap.
518
+ * - 'bordered': Dengan bingkai border solid luar.
519
+ * - 'card': Dengan elevasi bayangan halus kartu.
520
+ * - 'flat': Minimalis tanpa border dan tanpa shadow.
521
+ * @default 'bordered'
522
+ */
523
+ variant?: "default" | "bordered" | "card" | "flat";
524
+
525
+ /**
526
+ * Menonaktifkan seluruh kalender dari interaksi.
527
+ * @default false
528
+ */
529
+ disabled?: boolean;
530
+
531
+ /**
532
+ * Mode hanya-baca (read-only).
533
+ * @default false
534
+ */
535
+ readOnly?: boolean;
536
+
537
+ /**
538
+ * Callback event saat nilai tanggal berubah.
539
+ */
540
+ onValueChange?: (value: any) => void;
541
+
542
+ /**
543
+ * Callback event native saat nilai berubah.
544
+ */
545
+ onChange?: (event: any) => void;
546
+
547
+ /**
548
+ * Callback saat navigasi bulan atau tahun berubah.
549
+ */
550
+ onMonthChange?: (month: number, year: number) => void;
551
+
552
+ /**
553
+ * Callback saat tahun tampilan kalender berubah.
554
+ */
555
+ onYearChange?: (month: number, year: number) => void;
556
+
557
+ /**
558
+ * Mode tampilan awal kalender ('day', 'month', 'year').
559
+ * @default 'day'
560
+ */
561
+ defaultViewMode?: "day" | "month" | "year";
562
+
563
+ /**
564
+ * Mode tampilan kalender saat ini ('day', 'month', 'year') untuk mode controlled.
565
+ */
566
+ viewMode?: "day" | "month" | "year";
567
+
568
+ /**
569
+ * Callback saat mode tampilan kalender berubah ('day', 'month', 'year').
570
+ */
571
+ onViewModeChange?: (mode: "day" | "month" | "year") => void;
572
+
573
+ /**
574
+ * Class CSS kustom untuk container utama.
575
+ */
576
+ className?: string;
577
+
578
+ /**
579
+ * Inline style CSS kustom untuk container utama.
580
+ */
581
+ style?: any;
582
+
583
+ /**
584
+ * Class CSS kustom untuk header kalender.
585
+ */
586
+ headerClass?: string;
587
+
588
+ /**
589
+ * Inline style kustom untuk header kalender.
590
+ */
591
+ headerStyle?: UiProps;
592
+
593
+ /**
594
+ * Class CSS kustom untuk tombol navigasi.
595
+ */
596
+ navBtnClass?: string;
597
+
598
+ /**
599
+ * Inline style kustom untuk tombol navigasi.
600
+ */
601
+ navBtnStyle?: UiProps;
602
+
603
+ /**
604
+ * Class CSS kustom untuk cell tanggal.
605
+ */
606
+ cellClass?: string;
607
+
608
+ /**
609
+ * Inline style kustom untuk cell tanggal.
610
+ */
611
+ cellStyle?: UiProps;
612
+
613
+ /**
614
+ * Kustomisasi visual granular UiProps.
615
+ */
616
+ ui?: UiProps;
617
+ }
618
+
619
+ const props = withDefaults(defineProps<CalendarProps>(), {
620
+ yearControls: true,
621
+ showOutsideDays: true,
622
+ fixedWeeks: true,
623
+ });
624
+ const viewYear = ref(
625
+ (() => {
626
+ if (props.year) return props.year;
627
+ if (props.defaultYear) return props.defaultYear;
628
+ const target = props.value !== undefined ? props.value : props.defaultValue;
629
+ if (target) {
630
+ const valStr =
631
+ props.range && typeof target === "object"
632
+ ? target.start
633
+ : props.multiple && Array.isArray(target)
634
+ ? target[0]
635
+ : target;
636
+ if (valStr instanceof Date && !isNaN(valStr.getTime()))
637
+ return valStr.getFullYear();
638
+ if (typeof valStr === "string" && valStr.trim()) {
639
+ const parts = valStr.split("-");
640
+ if (parts.length === 3) {
641
+ const y = parseInt(parts[0], 10);
642
+ if (!isNaN(y)) return y;
643
+ }
644
+ }
645
+ }
646
+ return new Date().getFullYear();
647
+ })()
648
+ );
649
+ const viewMonth = ref(
650
+ (() => {
651
+ if (props.month) return props.month;
652
+ if (props.defaultMonth) return props.defaultMonth;
653
+ const target = props.value !== undefined ? props.value : props.defaultValue;
654
+ if (target) {
655
+ const valStr =
656
+ props.range && typeof target === "object"
657
+ ? target.start
658
+ : props.multiple && Array.isArray(target)
659
+ ? target[0]
660
+ : target;
661
+ if (valStr instanceof Date && !isNaN(valStr.getTime()))
662
+ return valStr.getMonth() + 1;
663
+ if (typeof valStr === "string" && valStr.trim()) {
664
+ const parts = valStr.split("-");
665
+ if (parts.length === 3) {
666
+ const m = parseInt(parts[1], 10);
667
+ if (!isNaN(m)) return m;
668
+ }
669
+ }
670
+ }
671
+ return new Date().getMonth() + 1;
672
+ })()
673
+ );
674
+ const currentViewMode = ref(props.viewMode || props.defaultViewMode || "day");
675
+ const decadeStart = ref(null);
676
+ const selectedSingle = ref(null);
677
+ const rangeStart = ref(null);
678
+ const rangeEnd = ref(null);
679
+ const hoverDate = ref(null);
680
+ const selectedMultiple = ref([]);
681
+ const lastParsedKey = ref(
682
+ (() => {
683
+ const target = props.value !== undefined ? props.value : props.defaultValue;
684
+ if (!target) return "";
685
+ const valStr =
686
+ props.range && typeof target === "object"
687
+ ? target.start
688
+ : props.multiple && Array.isArray(target)
689
+ ? target[0]
690
+ : target;
691
+ return valStr instanceof Date
692
+ ? "" + valStr.getTime()
693
+ : typeof valStr === "string"
694
+ ? valStr
695
+ : "";
696
+ })()
697
+ );
698
+
699
+ onUpdated(() => {
700
+ const target = props.value !== undefined ? props.value : props.defaultValue;
701
+ const valStr = target
702
+ ? props.range && typeof target === "object"
703
+ ? target.start
704
+ : props.multiple && Array.isArray(target)
705
+ ? target[0]
706
+ : target
707
+ : null;
708
+ const valKey =
709
+ valStr instanceof Date
710
+ ? "" + valStr.getTime()
711
+ : typeof valStr === "string"
712
+ ? valStr
713
+ : "";
714
+ if (valKey !== lastParsedKey.value) {
715
+ lastParsedKey.value = valKey;
716
+ if (valStr) {
717
+ const parsed = parseDate(valStr);
718
+ if (parsed) {
719
+ if (!props.year) {
720
+ viewYear.value = parsed.getFullYear();
721
+ }
722
+ if (!props.month) {
723
+ viewMonth.value = parsed.getMonth() + 1;
724
+ }
725
+ }
726
+ }
727
+ }
728
+ });
729
+
730
+ function initialParsed() {
731
+ const target = props.value !== undefined ? props.value : props.defaultValue;
732
+ if (!target) return null;
733
+ if (props.range && typeof target === "object") {
734
+ const s = target.start;
735
+ if (s instanceof Date) return isNaN(s.getTime()) ? null : s;
736
+ if (typeof s === "string" && s.trim()) {
737
+ const parts = s.split("-");
738
+ if (parts.length === 3) {
739
+ const y = parseInt(parts[0], 10);
740
+ const m = parseInt(parts[1], 10) - 1;
741
+ const d = parseInt(parts[2], 10);
742
+ const res = new Date(y, m, d);
743
+ return isNaN(res.getTime()) ? null : res;
744
+ }
745
+ const p = new Date(s);
746
+ return isNaN(p.getTime()) ? null : p;
747
+ }
748
+ } else if (props.multiple && Array.isArray(target) && target.length > 0) {
749
+ const first = target[0];
750
+ if (first instanceof Date) return isNaN(first.getTime()) ? null : first;
751
+ if (typeof first === "string" && first.trim()) {
752
+ const parts = first.split("-");
753
+ if (parts.length === 3) {
754
+ const y = parseInt(parts[0], 10);
755
+ const m = parseInt(parts[1], 10) - 1;
756
+ const d = parseInt(parts[2], 10);
757
+ const res = new Date(y, m, d);
758
+ return isNaN(res.getTime()) ? null : res;
759
+ }
760
+ }
761
+ } else if (typeof target === "string" && target.trim()) {
762
+ const parts = target.split("-");
763
+ if (parts.length === 3) {
764
+ const y = parseInt(parts[0], 10);
765
+ const m = parseInt(parts[1], 10) - 1;
766
+ const d = parseInt(parts[2], 10);
767
+ const res = new Date(y, m, d);
768
+ return isNaN(res.getTime()) ? null : res;
769
+ }
770
+ const p = new Date(target);
771
+ return isNaN(p.getTime()) ? null : p;
772
+ } else if (target instanceof Date && !isNaN(target.getTime())) {
773
+ return target;
774
+ }
775
+ return null;
776
+ }
777
+ function padZero(num: number) {
778
+ return num < 10 ? "0" + num : "" + num;
779
+ }
780
+ function toDateString(d: Date) {
781
+ if (!d || isNaN(d.getTime())) {
782
+ const now = new Date();
783
+ return (
784
+ now.getFullYear() +
785
+ "-" +
786
+ padZero(now.getMonth() + 1) +
787
+ "-" +
788
+ padZero(now.getDate())
789
+ );
790
+ }
791
+ return (
792
+ d.getFullYear() +
793
+ "-" +
794
+ padZero(d.getMonth() + 1) +
795
+ "-" +
796
+ padZero(d.getDate())
797
+ );
798
+ }
799
+ function parseDate(val: any) {
800
+ if (!val) return null;
801
+ if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
802
+ if (typeof val === "string") {
803
+ const parts = val.split("-");
804
+ if (parts.length === 3) {
805
+ const y = parseInt(parts[0], 10);
806
+ const m = parseInt(parts[1], 10) - 1;
807
+ const d = parseInt(parts[2], 10);
808
+ const res = new Date(y, m, d);
809
+ return isNaN(res.getTime()) ? null : res;
810
+ }
811
+ const parsed = new Date(val);
812
+ return isNaN(parsed.getTime()) ? null : parsed;
813
+ }
814
+ return null;
815
+ }
816
+ function getViewYear(monthOffset: number = 0) {
817
+ const now = new Date();
818
+ let m =
819
+ (props.month || viewMonth.value || now.getMonth() + 1) - 1 + monthOffset;
820
+ let y = props.year || viewYear.value || now.getFullYear();
821
+ if (isNaN(m)) m = now.getMonth() + monthOffset;
822
+ if (isNaN(y)) y = now.getFullYear();
823
+ while (m > 11) {
824
+ m -= 12;
825
+ y += 1;
826
+ }
827
+ while (m < 0) {
828
+ m += 12;
829
+ y -= 1;
830
+ }
831
+ return y;
832
+ }
833
+ function getViewMonth(monthOffset: number = 0) {
834
+ const now = new Date();
835
+ let m =
836
+ (props.month || viewMonth.value || now.getMonth() + 1) - 1 + monthOffset;
837
+ if (isNaN(m)) m = now.getMonth() + monthOffset;
838
+ while (m > 11) {
839
+ m -= 12;
840
+ }
841
+ while (m < 0) {
842
+ m += 12;
843
+ }
844
+ return m + 1;
845
+ }
846
+ function getMonthLabel(year: number, month: number) {
847
+ const loc = props.locale || "id-ID";
848
+ const d = new Date(year, month - 1, 1);
849
+ return d.toLocaleDateString(loc, {
850
+ month: "long",
851
+ year: "numeric",
852
+ });
853
+ }
854
+ function getMonthOnlyLabel(year: number, month: number) {
855
+ const loc = props.locale || "id-ID";
856
+ const d = new Date(year, month - 1, 1);
857
+ return d.toLocaleDateString(loc, {
858
+ month: "long",
859
+ });
860
+ }
861
+ function getViewMode() {
862
+ return props.viewMode || currentViewMode.value || "day";
863
+ }
864
+ function setViewMode(mode: "day" | "month" | "year") {
865
+ currentViewMode.value = mode;
866
+ if (props.onViewModeChange) {
867
+ props.onViewModeChange(mode);
868
+ }
869
+ }
870
+ function openMonthView() {
871
+ if (props.disabled) return;
872
+ setViewMode("month");
873
+ }
874
+ function openYearView() {
875
+ if (props.disabled) return;
876
+ setViewMode("year");
877
+ }
878
+ function getDecadeStart() {
879
+ if (decadeStart.value !== null) {
880
+ return decadeStart.value;
881
+ }
882
+ const y = props.year || viewYear.value;
883
+ return Math.floor(y / 10) * 10;
884
+ }
885
+ function getDecadeLabel() {
886
+ const start = getDecadeStart();
887
+ const end = start + 11;
888
+ return start + " - " + end;
889
+ }
890
+ function prevDecade() {
891
+ if (props.disabled) return;
892
+ decadeStart.value = getDecadeStart() - 12;
893
+ }
894
+ function nextDecade() {
895
+ if (props.disabled) return;
896
+ decadeStart.value = getDecadeStart() + 12;
897
+ }
898
+ function getYearsList() {
899
+ const start = getDecadeStart();
900
+ const currentY = props.year || viewYear.value;
901
+ const actualY = new Date().getFullYear();
902
+ const list: any[] = [];
903
+ for (let i = 0; i < 12; i++) {
904
+ const y = start + i;
905
+ list.push({
906
+ year: y,
907
+ label: "" + y,
908
+ isSelected: y === currentY,
909
+ isCurrent: y === actualY,
910
+ });
911
+ }
912
+ return list;
913
+ }
914
+ function selectYear(year: number) {
915
+ if (props.disabled) return;
916
+ viewYear.value = year;
917
+ decadeStart.value = null;
918
+ setViewMode("month");
919
+ if (props.onYearChange) {
920
+ props.onYearChange(getViewMonth(), year);
921
+ }
922
+ if (props.onMonthChange) {
923
+ props.onMonthChange(getViewMonth(), year);
924
+ }
925
+ }
926
+ function getMonthsList() {
927
+ const currentY = props.year || viewYear.value;
928
+ const currentM = props.month || viewMonth.value;
929
+ const actualNow = new Date();
930
+ const actualY = actualNow.getFullYear();
931
+ const actualM = actualNow.getMonth() + 1;
932
+ const loc = props.locale || "id-ID";
933
+ const list: any[] = [];
934
+ for (let m = 1; m <= 12; m++) {
935
+ const d = new Date(currentY, m - 1, 1);
936
+ const shortName = d.toLocaleDateString(loc, {
937
+ month: "short",
938
+ });
939
+ const fullName = d.toLocaleDateString(loc, {
940
+ month: "long",
941
+ });
942
+ list.push({
943
+ month: m,
944
+ label: shortName,
945
+ fullName: fullName,
946
+ isSelected: m === currentM,
947
+ isCurrent: currentY === actualY && m === actualM,
948
+ });
949
+ }
950
+ return list;
951
+ }
952
+ function selectMonth(month: number) {
953
+ if (props.disabled) return;
954
+ viewMonth.value = month;
955
+ setViewMode("day");
956
+ if (props.onMonthChange) {
957
+ props.onMonthChange(month, getViewYear());
958
+ }
959
+ }
960
+ function getWeekDays() {
961
+ const startOn = props.weekStartsOn !== undefined ? props.weekStartsOn : 1;
962
+ const idLabels = ["Min", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab"];
963
+ const enLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
964
+ const loc = props.locale || "id-ID";
965
+ const base = loc.toLowerCase().indexOf("en") === 0 ? enLabels : idLabels;
966
+ const result: string[] = [];
967
+ for (let i = 0; i < 7; i++) {
968
+ const idx = (startOn + i) % 7;
969
+ result.push(base[idx]);
970
+ }
971
+ return result;
972
+ }
973
+ function prevMonth() {
974
+ if (props.disabled) return;
975
+ let nextM = viewMonth.value - 1;
976
+ let nextY = viewYear.value;
977
+ if (nextM < 1) {
978
+ nextM = 12;
979
+ nextY -= 1;
980
+ }
981
+ viewMonth.value = nextM;
982
+ viewYear.value = nextY;
983
+ if (props.onMonthChange) {
984
+ props.onMonthChange(nextM, nextY);
985
+ }
986
+ }
987
+ function nextMonth() {
988
+ if (props.disabled) return;
989
+ let nextM = viewMonth.value + 1;
990
+ let nextY = viewYear.value;
991
+ if (nextM > 12) {
992
+ nextM = 1;
993
+ nextY += 1;
994
+ }
995
+ viewMonth.value = nextM;
996
+ viewYear.value = nextY;
997
+ if (props.onMonthChange) {
998
+ props.onMonthChange(nextM, nextY);
999
+ }
1000
+ }
1001
+ function prevYear() {
1002
+ if (props.disabled) return;
1003
+ const nextY = viewYear.value - 1;
1004
+ viewYear.value = nextY;
1005
+ if (props.onMonthChange) {
1006
+ props.onMonthChange(viewMonth.value, nextY);
1007
+ }
1008
+ }
1009
+ function nextYear() {
1010
+ if (props.disabled) return;
1011
+ const nextY = viewYear.value + 1;
1012
+ viewYear.value = nextY;
1013
+ if (props.onMonthChange) {
1014
+ props.onMonthChange(viewMonth.value, nextY);
1015
+ }
1016
+ }
1017
+ function goToToday() {
1018
+ if (props.disabled) return;
1019
+ const today = new Date();
1020
+ viewYear.value = today.getFullYear();
1021
+ viewMonth.value = today.getMonth() + 1;
1022
+ decadeStart.value = null;
1023
+ setViewMode("day");
1024
+ handleSelectDate(toDateString(today));
1025
+ if (props.onMonthChange) {
1026
+ props.onMonthChange(viewMonth.value, viewYear.value);
1027
+ }
1028
+ }
1029
+ function isToday(dateStr: string) {
1030
+ const todayStr = toDateString(new Date());
1031
+ return dateStr === todayStr;
1032
+ }
1033
+ function isDateDisabledCheck(date: Date) {
1034
+ if (props.disabled) return true;
1035
+ const minDate = parseDate(props.min || props.minValue);
1036
+ if (
1037
+ minDate &&
1038
+ date <
1039
+ new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate())
1040
+ ) {
1041
+ return true;
1042
+ }
1043
+ const maxDate = parseDate(props.max || props.maxValue);
1044
+ if (
1045
+ maxDate &&
1046
+ date >
1047
+ new Date(
1048
+ maxDate.getFullYear(),
1049
+ maxDate.getMonth(),
1050
+ maxDate.getDate(),
1051
+ 23,
1052
+ 59,
1053
+ 59
1054
+ )
1055
+ ) {
1056
+ return true;
1057
+ }
1058
+ if (props.isDateDisabled) {
1059
+ return props.isDateDisabled(date);
1060
+ }
1061
+ return false;
1062
+ }
1063
+ function isDateUnavailableCheck(date: Date) {
1064
+ if (props.isDateUnavailable) {
1065
+ return props.isDateUnavailable(date);
1066
+ }
1067
+ return false;
1068
+ }
1069
+ function getDateMarker(dateStr: string) {
1070
+ if (!props.dateMarkers || !props.dateMarkers.length) return null;
1071
+ for (let i = 0; i < props.dateMarkers.length; i++) {
1072
+ if (props.dateMarkers[i].date === dateStr) {
1073
+ return props.dateMarkers[i];
1074
+ }
1075
+ }
1076
+ return null;
1077
+ }
1078
+ function isSelected(dateStr: string) {
1079
+ if (props.range) {
1080
+ return isRangeStart(dateStr) || isRangeEnd(dateStr);
1081
+ }
1082
+ if (props.multiple) {
1083
+ const rawList =
1084
+ props.value !== undefined
1085
+ ? Array.isArray(props.value)
1086
+ ? props.value
1087
+ : []
1088
+ : selectedMultiple.value;
1089
+ const strList = rawList.map((item: any) =>
1090
+ typeof item === "string"
1091
+ ? item
1092
+ : item instanceof Date
1093
+ ? toDateString(item)
1094
+ : ""
1095
+ );
1096
+ return strList.indexOf(dateStr) !== -1;
1097
+ }
1098
+ const current =
1099
+ props.value !== undefined
1100
+ ? typeof props.value === "string"
1101
+ ? props.value
1102
+ : props.value instanceof Date
1103
+ ? toDateString(props.value)
1104
+ : null
1105
+ : selectedSingle.value;
1106
+ if (!current && props.defaultValue && !selectedSingle.value) {
1107
+ const def =
1108
+ typeof props.defaultValue === "string"
1109
+ ? props.defaultValue
1110
+ : props.defaultValue instanceof Date
1111
+ ? toDateString(props.defaultValue)
1112
+ : null;
1113
+ return def === dateStr;
1114
+ }
1115
+ return current === dateStr;
1116
+ }
1117
+ function isRangeStart(dateStr: string) {
1118
+ if (!props.range) return false;
1119
+ const raw =
1120
+ props.value && props.value.start
1121
+ ? props.value.start
1122
+ : props.defaultValue && props.defaultValue.start
1123
+ ? props.defaultValue.start
1124
+ : rangeStart.value;
1125
+ const start =
1126
+ typeof raw === "string"
1127
+ ? raw
1128
+ : raw instanceof Date
1129
+ ? toDateString(raw)
1130
+ : null;
1131
+ return start === dateStr;
1132
+ }
1133
+ function isRangeEnd(dateStr: string) {
1134
+ if (!props.range) return false;
1135
+ const raw =
1136
+ props.value && props.value.end
1137
+ ? props.value.end
1138
+ : props.defaultValue && props.defaultValue.end
1139
+ ? props.defaultValue.end
1140
+ : rangeEnd.value;
1141
+ const end =
1142
+ typeof raw === "string"
1143
+ ? raw
1144
+ : raw instanceof Date
1145
+ ? toDateString(raw)
1146
+ : null;
1147
+ return end === dateStr;
1148
+ }
1149
+ function isInRange(dateStr: string) {
1150
+ if (!props.range) return false;
1151
+ const rawStart =
1152
+ props.value && props.value.start
1153
+ ? props.value.start
1154
+ : props.defaultValue && props.defaultValue.start
1155
+ ? props.defaultValue.start
1156
+ : rangeStart.value;
1157
+ let rawEnd =
1158
+ props.value && props.value.end
1159
+ ? props.value.end
1160
+ : props.defaultValue && props.defaultValue.end
1161
+ ? props.defaultValue.end
1162
+ : rangeEnd.value;
1163
+ const start =
1164
+ typeof rawStart === "string"
1165
+ ? rawStart
1166
+ : rawStart instanceof Date
1167
+ ? toDateString(rawStart)
1168
+ : null;
1169
+ let end =
1170
+ typeof rawEnd === "string"
1171
+ ? rawEnd
1172
+ : rawEnd instanceof Date
1173
+ ? toDateString(rawEnd)
1174
+ : null;
1175
+ if (start && !end && hoverDate.value) {
1176
+ end = hoverDate.value;
1177
+ }
1178
+ if (!start || !end) return false;
1179
+ const minD = start < end ? start : end;
1180
+ const maxD = start < end ? end : start;
1181
+ return dateStr > minD && dateStr < maxD;
1182
+ }
1183
+ function getCellRangeClass(dateStr: string) {
1184
+ if (!props.range) return "";
1185
+ const isStart = isRangeStart(dateStr);
1186
+ const isEnd = isRangeEnd(dateStr);
1187
+ const inRange = isInRange(dateStr);
1188
+ if (isStart && isEnd) return "ttdk-calendar-cell-range-single";
1189
+ if (isStart) return "ttdk-calendar-cell-range-start";
1190
+ if (isEnd) return "ttdk-calendar-cell-range-end";
1191
+ if (inRange) return "ttdk-calendar-cell-range";
1192
+ return "";
1193
+ }
1194
+ function handleDayHover(dateStr: string) {
1195
+ if (!props.range || props.disabled || props.readOnly) return;
1196
+ const rawStart =
1197
+ props.value && props.value.start
1198
+ ? props.value.start
1199
+ : props.defaultValue && props.defaultValue.start
1200
+ ? props.defaultValue.start
1201
+ : rangeStart.value;
1202
+ const rawEnd =
1203
+ props.value && props.value.end
1204
+ ? props.value.end
1205
+ : props.defaultValue && props.defaultValue.end
1206
+ ? props.defaultValue.end
1207
+ : rangeEnd.value;
1208
+ const start =
1209
+ typeof rawStart === "string"
1210
+ ? rawStart
1211
+ : rawStart instanceof Date
1212
+ ? toDateString(rawStart)
1213
+ : null;
1214
+ const end =
1215
+ typeof rawEnd === "string"
1216
+ ? rawEnd
1217
+ : rawEnd instanceof Date
1218
+ ? toDateString(rawEnd)
1219
+ : null;
1220
+ if (start && !end) {
1221
+ hoverDate.value = dateStr;
1222
+ }
1223
+ }
1224
+ function handleDayHoverLeave() {
1225
+ if (hoverDate.value) {
1226
+ hoverDate.value = null;
1227
+ }
1228
+ }
1229
+ function handleSelectDate(dateStr: string) {
1230
+ if (props.disabled || props.readOnly) return;
1231
+ const dateObj = parseDate(dateStr);
1232
+ if (!dateObj) return;
1233
+ if (isDateDisabledCheck(dateObj) || isDateUnavailableCheck(dateObj)) {
1234
+ return;
1235
+ }
1236
+ if (props.range) {
1237
+ const rawStart =
1238
+ props.value && props.value.start
1239
+ ? props.value.start
1240
+ : props.defaultValue && props.defaultValue.start
1241
+ ? props.defaultValue.start
1242
+ : rangeStart.value;
1243
+ const rawEnd =
1244
+ props.value && props.value.end
1245
+ ? props.value.end
1246
+ : props.defaultValue && props.defaultValue.end
1247
+ ? props.defaultValue.end
1248
+ : rangeEnd.value;
1249
+ const curStart =
1250
+ typeof rawStart === "string"
1251
+ ? rawStart
1252
+ : rawStart instanceof Date
1253
+ ? toDateString(rawStart)
1254
+ : null;
1255
+ const curEnd =
1256
+ typeof rawEnd === "string"
1257
+ ? rawEnd
1258
+ : rawEnd instanceof Date
1259
+ ? toDateString(rawEnd)
1260
+ : null;
1261
+ if (!curStart || (curStart && curEnd)) {
1262
+ rangeStart.value = dateStr;
1263
+ rangeEnd.value = null;
1264
+ hoverDate.value = null;
1265
+ lastParsedKey.value = dateStr;
1266
+ if (props.onValueChange) {
1267
+ props.onValueChange({
1268
+ start: dateStr,
1269
+ end: null,
1270
+ });
1271
+ }
1272
+ } else {
1273
+ let startRes = curStart;
1274
+ let endRes = dateStr;
1275
+ if (dateStr < curStart) {
1276
+ startRes = dateStr;
1277
+ endRes = curStart;
1278
+ }
1279
+ rangeStart.value = startRes;
1280
+ rangeEnd.value = endRes;
1281
+ hoverDate.value = null;
1282
+ if (props.onValueChange) {
1283
+ props.onValueChange({
1284
+ start: startRes,
1285
+ end: endRes,
1286
+ });
1287
+ }
1288
+ }
1289
+ } else if (props.multiple) {
1290
+ let currentList =
1291
+ props.value !== undefined
1292
+ ? Array.isArray(props.value)
1293
+ ? [...props.value]
1294
+ : []
1295
+ : [...selectedMultiple.value];
1296
+ const strList = currentList.map((item: any) =>
1297
+ typeof item === "string" ? item : toDateString(item)
1298
+ );
1299
+ const idx = strList.indexOf(dateStr);
1300
+ if (idx >= 0) {
1301
+ strList.splice(idx, 1);
1302
+ } else {
1303
+ strList.push(dateStr);
1304
+ }
1305
+ selectedMultiple.value = strList;
1306
+ if (props.onValueChange) {
1307
+ props.onValueChange(strList);
1308
+ }
1309
+ } else {
1310
+ selectedSingle.value = dateStr;
1311
+ if (dateObj) {
1312
+ viewYear.value = dateObj.getFullYear();
1313
+ viewMonth.value = dateObj.getMonth() + 1;
1314
+ lastParsedKey.value = dateStr;
1315
+ }
1316
+ if (props.onValueChange) {
1317
+ props.onValueChange(dateStr);
1318
+ }
1319
+ }
1320
+ if (props.onChange) {
1321
+ props.onChange({
1322
+ target: {
1323
+ value: dateStr,
1324
+ },
1325
+ value: dateStr,
1326
+ });
1327
+ }
1328
+ }
1329
+ function getMonthWeeks(year: number, month: number) {
1330
+ const startOn = props.weekStartsOn !== undefined ? props.weekStartsOn : 1;
1331
+ const showOutside = props.showOutsideDays !== false;
1332
+ const fixed = props.fixedWeeks !== false;
1333
+ const todayStr = toDateString(new Date());
1334
+
1335
+ // Precompute disabled bounds
1336
+ const minDateObj =
1337
+ props.min || props.minValue ? parseDate(props.min || props.minValue) : null;
1338
+ const maxDateObj =
1339
+ props.max || props.maxValue ? parseDate(props.max || props.maxValue) : null;
1340
+ const minTime = minDateObj
1341
+ ? new Date(
1342
+ minDateObj.getFullYear(),
1343
+ minDateObj.getMonth(),
1344
+ minDateObj.getDate()
1345
+ ).getTime()
1346
+ : null;
1347
+ const maxTime = maxDateObj
1348
+ ? new Date(
1349
+ maxDateObj.getFullYear(),
1350
+ maxDateObj.getMonth(),
1351
+ maxDateObj.getDate(),
1352
+ 23,
1353
+ 59,
1354
+ 59
1355
+ ).getTime()
1356
+ : null;
1357
+
1358
+ // Precompute single selection
1359
+ const selectedSingleStr =
1360
+ !props.range && !props.multiple
1361
+ ? props.value !== undefined
1362
+ ? typeof props.value === "string"
1363
+ ? props.value
1364
+ : props.value instanceof Date
1365
+ ? toDateString(props.value)
1366
+ : null
1367
+ : selectedSingle.value ||
1368
+ (props.defaultValue
1369
+ ? typeof props.defaultValue === "string"
1370
+ ? props.defaultValue
1371
+ : toDateString(props.defaultValue)
1372
+ : null)
1373
+ : null;
1374
+
1375
+ // Precompute range selection
1376
+ let rStart: string | null = null;
1377
+ let rEnd: string | null = null;
1378
+ if (props.range) {
1379
+ const rawS =
1380
+ props.value && props.value.start
1381
+ ? props.value.start
1382
+ : props.defaultValue && props.defaultValue.start
1383
+ ? props.defaultValue.start
1384
+ : rangeStart.value;
1385
+ const rawE =
1386
+ props.value && props.value.end
1387
+ ? props.value.end
1388
+ : props.defaultValue && props.defaultValue.end
1389
+ ? props.defaultValue.end
1390
+ : rangeEnd.value;
1391
+ rStart =
1392
+ typeof rawS === "string"
1393
+ ? rawS
1394
+ : rawS instanceof Date
1395
+ ? toDateString(rawS)
1396
+ : null;
1397
+ rEnd =
1398
+ typeof rawE === "string"
1399
+ ? rawE
1400
+ : rawE instanceof Date
1401
+ ? toDateString(rawE)
1402
+ : null;
1403
+ if (rStart && !rEnd && hoverDate.value) {
1404
+ rEnd = hoverDate.value;
1405
+ }
1406
+ }
1407
+ const rangeMin = rStart && rEnd ? (rStart < rEnd ? rStart : rEnd) : null;
1408
+ const rangeMax = rStart && rEnd ? (rStart < rEnd ? rEnd : rStart) : null;
1409
+
1410
+ // Precompute multiple selection Set
1411
+ const multipleSet: any = props.multiple
1412
+ ? new Set(
1413
+ (props.value !== undefined
1414
+ ? Array.isArray(props.value)
1415
+ ? props.value
1416
+ : []
1417
+ : selectedMultiple.value
1418
+ ).map((item: any) =>
1419
+ typeof item === "string"
1420
+ ? item
1421
+ : item instanceof Date
1422
+ ? toDateString(item)
1423
+ : ""
1424
+ )
1425
+ )
1426
+ : null;
1427
+
1428
+ // Precompute marker map
1429
+ const markerMap: any =
1430
+ props.dateMarkers && props.dateMarkers.length
1431
+ ? new Map(props.dateMarkers.map((m: any) => [m.date, m]))
1432
+ : null;
1433
+ const firstDay = new Date(year, month - 1, 1);
1434
+ const lastDay = new Date(year, month, 0);
1435
+ const totalDays = lastDay.getDate();
1436
+ let firstDayIndex = firstDay.getDay() - startOn;
1437
+ if (firstDayIndex < 0) firstDayIndex += 7;
1438
+ const prevMonthLastDay = new Date(year, month - 1, 0).getDate();
1439
+ const days: any[] = [];
1440
+ const buildCell = (d: number, dDate: Date, isCurMonth: boolean) => {
1441
+ const dateStr = toDateString(dDate);
1442
+ const dTime = dDate.getTime();
1443
+ const isDisabled =
1444
+ props.disabled ||
1445
+ (minTime !== null && dTime < minTime) ||
1446
+ (maxTime !== null && dTime > maxTime) ||
1447
+ (props.isDateDisabled ? props.isDateDisabled(dDate) : false);
1448
+ const isUnavailable = props.isDateUnavailable
1449
+ ? props.isDateUnavailable(dDate)
1450
+ : false;
1451
+ const isRangeS = props.range ? rStart === dateStr : false;
1452
+ const isRangeE = props.range ? rEnd === dateStr : false;
1453
+ const inRange =
1454
+ rangeMin !== null &&
1455
+ rangeMax !== null &&
1456
+ dateStr > rangeMin &&
1457
+ dateStr < rangeMax;
1458
+ const isSel = props.range
1459
+ ? isRangeS || isRangeE
1460
+ : props.multiple
1461
+ ? multipleSet
1462
+ ? multipleSet.has(dateStr)
1463
+ : false
1464
+ : dateStr === selectedSingleStr;
1465
+ const rangeCls = !props.range
1466
+ ? ""
1467
+ : (isRangeS && isRangeE) || (isRangeS && !rEnd)
1468
+ ? "ttdk-calendar-cell-range-single"
1469
+ : isRangeS
1470
+ ? "ttdk-calendar-cell-range-start"
1471
+ : isRangeE
1472
+ ? "ttdk-calendar-cell-range-end"
1473
+ : inRange
1474
+ ? "ttdk-calendar-cell-range"
1475
+ : "";
1476
+ const markerObj = markerMap ? markerMap.get(dateStr) || null : null;
1477
+ return {
1478
+ day: d,
1479
+ dateStr: dateStr,
1480
+ isCurrentMonth: isCurMonth,
1481
+ isOutside: !isCurMonth,
1482
+ date: dDate,
1483
+ disabled: isDisabled,
1484
+ unavailable: isUnavailable,
1485
+ today: dateStr === todayStr,
1486
+ selected: isSel,
1487
+ inRange: inRange,
1488
+ isRangeStart: isRangeS,
1489
+ isRangeEnd: isRangeE,
1490
+ rangeClass: rangeCls,
1491
+ marker: markerObj,
1492
+ };
1493
+ };
1494
+ for (let i = firstDayIndex - 1; i >= 0; i--) {
1495
+ const d = prevMonthLastDay - i;
1496
+ const prevMonthDate = new Date(year, month - 2, d);
1497
+ days.push(buildCell(d, prevMonthDate, false));
1498
+ }
1499
+ for (let i = 1; i <= totalDays; i++) {
1500
+ const curDate = new Date(year, month - 1, i);
1501
+ days.push(buildCell(i, curDate, true));
1502
+ }
1503
+ const targetLength = fixed ? 42 : Math.ceil(days.length / 7) * 7;
1504
+ let nextDayNum = 1;
1505
+ while (days.length < targetLength) {
1506
+ const nextMonthDate = new Date(year, month, nextDayNum);
1507
+ days.push(buildCell(nextDayNum, nextMonthDate, false));
1508
+ nextDayNum++;
1509
+ }
1510
+ const weeks: any[][] = [];
1511
+ for (let i = 0; i < days.length; i += 7) {
1512
+ weeks.push(days.slice(i, i + 7));
1513
+ }
1514
+ return weeks;
1515
+ }
1516
+ function getMonths() {
1517
+ const count =
1518
+ props.numberOfMonths && props.numberOfMonths > 1 ? props.numberOfMonths : 1;
1519
+ const list: any[] = [];
1520
+ for (let i = 0; i < count; i++) {
1521
+ const y = getViewYear(i);
1522
+ const m = getViewMonth(i);
1523
+ list.push({
1524
+ offset: i,
1525
+ year: y,
1526
+ month: m,
1527
+ label: getMonthLabel(y, m),
1528
+ monthName: getMonthOnlyLabel(y, m),
1529
+ isFirst: i === 0,
1530
+ isLast: i === count - 1,
1531
+ weeks: getMonthWeeks(y, m),
1532
+ });
1533
+ }
1534
+ return list;
1535
+ }
1536
+ </script>