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