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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/AI_GUIDE.md +6518 -0
  2. package/README.md +106 -2
  3. package/llms.txt +6518 -0
  4. package/package.json +1099 -4
  5. package/src/core/index.ts +2 -0
  6. package/src/core/tailwind-preset.cjs +62 -0
  7. package/src/core/tailwind-preset.ts +85 -0
  8. package/src/core/theme/tokens.css +663 -0
  9. package/src/core/types.ts +836 -0
  10. package/src/index.ts +17 -0
  11. package/src/tailwind-preset.cjs +62 -0
  12. package/src/tailwind-preset.ts +85 -0
  13. package/src/types.ts +1 -0
  14. package/src/web/accordion/AI_GUIDE.md +149 -0
  15. package/src/web/accordion/accordion.css +1 -0
  16. package/src/web/accordion/accordion.tsx +430 -0
  17. package/src/web/accordion/index.ts +3 -0
  18. package/src/web/accordion/manifest.json +351 -0
  19. package/src/web/alert/AI_GUIDE.md +139 -0
  20. package/src/web/alert/alert.css +1 -0
  21. package/src/web/alert/alert.tsx +318 -0
  22. package/src/web/alert/index.ts +3 -0
  23. package/src/web/alert/manifest.json +240 -0
  24. package/src/web/avatar/AI_GUIDE.md +118 -0
  25. package/src/web/avatar/avatar.css +1 -0
  26. package/src/web/avatar/avatar.tsx +233 -0
  27. package/src/web/avatar/index.ts +3 -0
  28. package/src/web/avatar/manifest.json +195 -0
  29. package/src/web/badge/AI_GUIDE.md +94 -0
  30. package/src/web/badge/badge.css +1 -0
  31. package/src/web/badge/badge.tsx +140 -0
  32. package/src/web/badge/index.ts +2 -0
  33. package/src/web/badge/manifest.json +141 -0
  34. package/src/web/banner/AI_GUIDE.md +130 -0
  35. package/src/web/banner/banner.css +1 -0
  36. package/src/web/banner/banner.tsx +334 -0
  37. package/src/web/banner/index.ts +3 -0
  38. package/src/web/banner/manifest.json +240 -0
  39. package/src/web/breadcrumb/AI_GUIDE.md +120 -0
  40. package/src/web/breadcrumb/breadcrumb.css +1 -0
  41. package/src/web/breadcrumb/breadcrumb.tsx +306 -0
  42. package/src/web/breadcrumb/index.ts +3 -0
  43. package/src/web/breadcrumb/manifest.json +225 -0
  44. package/src/web/button/AI_GUIDE.md +121 -0
  45. package/src/web/button/button.css +1 -0
  46. package/src/web/button/button.tsx +287 -0
  47. package/src/web/button/index.ts +2 -0
  48. package/src/web/button/manifest.json +177 -0
  49. package/src/web/calendar/AI_GUIDE.md +151 -0
  50. package/src/web/calendar/calendar.css +1 -0
  51. package/src/web/calendar/calendar.tsx +1611 -0
  52. package/src/web/calendar/index.ts +3 -0
  53. package/src/web/calendar/manifest.json +470 -0
  54. package/src/web/card/AI_GUIDE.md +133 -0
  55. package/src/web/card/card.css +1 -0
  56. package/src/web/card/card.tsx +408 -0
  57. package/src/web/card/index.ts +4 -0
  58. package/src/web/card/manifest.json +357 -0
  59. package/src/web/checkbox/AI_GUIDE.md +146 -0
  60. package/src/web/checkbox/checkbox.css +1 -0
  61. package/src/web/checkbox/checkbox.tsx +307 -0
  62. package/src/web/checkbox/index.ts +2 -0
  63. package/src/web/checkbox/manifest.json +294 -0
  64. package/src/web/chip/AI_GUIDE.md +186 -0
  65. package/src/web/chip/chip.css +1 -0
  66. package/src/web/chip/chip.tsx +230 -0
  67. package/src/web/chip/index.ts +2 -0
  68. package/src/web/chip/manifest.json +186 -0
  69. package/src/web/dropdown/AI_GUIDE.md +167 -0
  70. package/src/web/dropdown/dropdown.css +1 -0
  71. package/src/web/dropdown/dropdown.tsx +1274 -0
  72. package/src/web/dropdown/index.ts +3 -0
  73. package/src/web/dropdown/manifest.json +576 -0
  74. package/src/web/flex/AI_GUIDE.md +113 -0
  75. package/src/web/flex/flex-item.tsx +97 -0
  76. package/src/web/flex/flex.css +1 -0
  77. package/src/web/flex/flex.tsx +108 -0
  78. package/src/web/flex/index.ts +5 -0
  79. package/src/web/flex/manifest.json +162 -0
  80. package/src/web/form/index.ts +7 -0
  81. package/src/web/grid/AI_GUIDE.md +112 -0
  82. package/src/web/grid/grid-item.tsx +112 -0
  83. package/src/web/grid/grid.css +1 -0
  84. package/src/web/grid/grid.tsx +99 -0
  85. package/src/web/grid/index.ts +5 -0
  86. package/src/web/grid/manifest.json +153 -0
  87. package/src/web/index.ts +86 -0
  88. package/src/web/input/AI_GUIDE.md +159 -0
  89. package/src/web/input/index.ts +3 -0
  90. package/src/web/input/input.css +1 -0
  91. package/src/web/input/input.tsx +537 -0
  92. package/src/web/input/manifest.json +465 -0
  93. package/src/web/input-number/AI_GUIDE.md +164 -0
  94. package/src/web/input-number/index.ts +3 -0
  95. package/src/web/input-number/input-number.css +1 -0
  96. package/src/web/input-number/input-number.tsx +1193 -0
  97. package/src/web/input-number/manifest.json +575 -0
  98. package/src/web/modal/AI_GUIDE.md +171 -0
  99. package/src/web/modal/index.ts +3 -0
  100. package/src/web/modal/manifest.json +375 -0
  101. package/src/web/modal/modal.css +1 -0
  102. package/src/web/modal/modal.tsx +571 -0
  103. package/src/web/pagination/AI_GUIDE.md +148 -0
  104. package/src/web/pagination/index.ts +21 -0
  105. package/src/web/pagination/manifest.json +432 -0
  106. package/src/web/pagination/pagination-content.tsx +45 -0
  107. package/src/web/pagination/pagination-ellipsis.tsx +54 -0
  108. package/src/web/pagination/pagination-first.tsx +155 -0
  109. package/src/web/pagination/pagination-item.tsx +45 -0
  110. package/src/web/pagination/pagination-last.tsx +155 -0
  111. package/src/web/pagination/pagination-link.tsx +121 -0
  112. package/src/web/pagination/pagination-next.tsx +158 -0
  113. package/src/web/pagination/pagination-previous.tsx +158 -0
  114. package/src/web/pagination/pagination.css +1 -0
  115. package/src/web/pagination/pagination.tsx +687 -0
  116. package/src/web/popover/AI_GUIDE.md +163 -0
  117. package/src/web/popover/index.ts +3 -0
  118. package/src/web/popover/manifest.json +396 -0
  119. package/src/web/popover/popover.css +1 -0
  120. package/src/web/popover/popover.tsx +702 -0
  121. package/src/web/radio/AI_GUIDE.md +155 -0
  122. package/src/web/radio/index.ts +2 -0
  123. package/src/web/radio/manifest.json +294 -0
  124. package/src/web/radio/radio.css +1 -0
  125. package/src/web/radio/radio.tsx +290 -0
  126. package/src/web/selectbox/AI_GUIDE.md +216 -0
  127. package/src/web/selectbox/index.ts +3 -0
  128. package/src/web/selectbox/manifest.json +477 -0
  129. package/src/web/selectbox/selectbox.css +1 -0
  130. package/src/web/selectbox/selectbox.tsx +1048 -0
  131. package/src/web/sonner/AI_GUIDE.md +98 -0
  132. package/src/web/sonner/index.ts +114 -0
  133. package/src/web/sonner/manifest.json +132 -0
  134. package/src/web/sonner/sonner.css +1 -0
  135. package/src/web/sonner/sonner.tsx +177 -0
  136. package/src/web/table/AI_GUIDE.md +145 -0
  137. package/src/web/table/index.ts +35 -0
  138. package/src/web/table/manifest.json +168 -0
  139. package/src/web/table/table-body.tsx +45 -0
  140. package/src/web/table/table-caption.tsx +45 -0
  141. package/src/web/table/table-cell.tsx +74 -0
  142. package/src/web/table/table-empty.tsx +108 -0
  143. package/src/web/table/table-footer.tsx +45 -0
  144. package/src/web/table/table-head.tsx +80 -0
  145. package/src/web/table/table-header.tsx +45 -0
  146. package/src/web/table/table-row.tsx +69 -0
  147. package/src/web/table/table.css +1 -0
  148. package/src/web/table/table.tsx +305 -0
  149. package/src/web/tabs/AI_GUIDE.md +150 -0
  150. package/src/web/tabs/index.ts +3 -0
  151. package/src/web/tabs/manifest.json +315 -0
  152. package/src/web/tabs/tabs.css +1 -0
  153. package/src/web/tabs/tabs.tsx +332 -0
  154. package/src/web/text/AI_GUIDE.md +129 -0
  155. package/src/web/text/index.ts +3 -0
  156. package/src/web/text/manifest.json +195 -0
  157. package/src/web/text/text.css +1 -0
  158. package/src/web/text/text.tsx +689 -0
  159. package/src/web/textarea/AI_GUIDE.md +141 -0
  160. package/src/web/textarea/index.ts +3 -0
  161. package/src/web/textarea/manifest.json +438 -0
  162. package/src/web/textarea/textarea.css +1 -0
  163. package/src/web/textarea/textarea.tsx +467 -0
  164. package/src/web/tooltip/AI_GUIDE.md +110 -0
  165. package/src/web/tooltip/index.ts +3 -0
  166. package/src/web/tooltip/manifest.json +222 -0
  167. package/src/web/tooltip/tooltip.css +1 -0
  168. package/src/web/tooltip/tooltip.tsx +423 -0
  169. package/src/webview/bottomsheet/AI_GUIDE.md +141 -0
  170. package/src/webview/bottomsheet/bottomsheet.css +1 -0
  171. package/src/webview/bottomsheet/bottomsheet.tsx +603 -0
  172. package/src/webview/bottomsheet/index.ts +4 -0
  173. package/src/webview/bottomsheet/manifest.json +377 -0
  174. package/src/webview/button/AI_GUIDE.md +79 -0
  175. package/src/webview/button/button-ios.css +1 -0
  176. package/src/webview/button/button-ios.tsx +138 -0
  177. package/src/webview/button/button-material.css +1 -0
  178. package/src/webview/button/button-material.tsx +138 -0
  179. package/src/webview/button/index.ts +7 -0
  180. package/src/webview/button/manifest.json +148 -0
  181. package/src/webview/index.ts +7 -0
  182. package/tailwind-preset.cjs +62 -0
  183. package/tailwind-preset.js +62 -0
  184. package/ttdk-manifest.json +10188 -0
@@ -0,0 +1,571 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useState, useRef, useEffect } from "react";
4
+ import { createPortal } from "react-dom";
5
+
6
+ export type ModalSize =
7
+ | "xs"
8
+ | "sm"
9
+ | "md"
10
+ | "lg"
11
+ | "xl"
12
+ | "2xl"
13
+ | "3xl"
14
+ | "4xl"
15
+ | "5xl"
16
+ | "full";
17
+ export type ModalUiProps = UiProps;
18
+ export interface ModalProps {
19
+ /**
20
+ * Status visibilitas modal (mode terkontrol / controlled state).
21
+ * Mendukung `v-model:open` / `v-model` pada Vue 3 dan controlled prop `open` pada React.
22
+ */
23
+ open?: boolean;
24
+
25
+ /**
26
+ * Status visibilitas awal saat pertama kali dirender (uncontrolled initial state).
27
+ * @default false
28
+ */
29
+ defaultOpen?: boolean;
30
+
31
+ /**
32
+ * Judul teks di bagian header modal.
33
+ */
34
+ title?: string;
35
+
36
+ /**
37
+ * Deskripsi penjelasan di bagian header modal di bawah judul.
38
+ */
39
+ description?: string;
40
+
41
+ /**
42
+ * Menampilkan tombol tutup 'X' di pojok kanan atas header.
43
+ * Berikan `false` untuk menyembunyikan tombol tutup.
44
+ * @default true
45
+ */
46
+ close?: boolean | any;
47
+
48
+ /**
49
+ * Ikon kustom untuk tombol tutup.
50
+ */
51
+ closeIcon?: any;
52
+
53
+ /**
54
+ * Menjadikan modal tampil dalam ukuran layar penuh (fullscreen 100vw x 100vh).
55
+ * @default false
56
+ */
57
+ fullscreen?: boolean;
58
+
59
+ /**
60
+ * Skala ukuran lebar maksimum modal dialog ('xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | 'full').
61
+ * @default 'md'
62
+ */
63
+ size?: ModalSize;
64
+
65
+ /**
66
+ * Mengizinkan modal ditutup saat pengguna mengeklik latar belakang (backdrop) atau menekan tombol `Escape`.
67
+ * @default true
68
+ */
69
+ dismissible?: boolean;
70
+
71
+ /**
72
+ * Mencegah modal ditutup saat klik di luar atau tombol `Escape` (alias selaras Nuxt UI prevent-close).
73
+ * Jika bernilai `true`, popover/modal tidak dapat ditutup kecuali melalui tombol aksi eksplisit di dalam modal.
74
+ * @default false
75
+ */
76
+ preventClose?: boolean;
77
+
78
+ /**
79
+ * Menampilkan lapisan latar belakang redup (backdrop overlay).
80
+ * @default true
81
+ */
82
+ overlay?: boolean;
83
+
84
+ /**
85
+ * Menjadikan modal sebagai dialog modal yang memusatkan fokus interaksi pengguna.
86
+ * @default true
87
+ */
88
+ modal?: boolean;
89
+
90
+ /**
91
+ * Mengaktifkan area konten body yang dapat digulir jika isi modal melebihi tinggi layar.
92
+ * @default true
93
+ */
94
+ scrollable?: boolean;
95
+
96
+ /**
97
+ * Mengunci scrolling halaman web latar belakang saat modal sedang terbuka (body scroll lock).
98
+ * @default true
99
+ */
100
+ preventScroll?: boolean;
101
+
102
+ /**
103
+ * Alias untuk `preventScroll`.
104
+ * @default true
105
+ */
106
+ lockScroll?: boolean;
107
+
108
+ /**
109
+ * Konten kustom untuk seluruh dialog (menggantikan header, body, dan footer default).
110
+ */
111
+ content?: any;
112
+
113
+ /**
114
+ * Konten kustom untuk bagian header modal.
115
+ */
116
+ header?: any;
117
+
118
+ /**
119
+ * Konten kustom untuk bagian isi utama (body) modal.
120
+ */
121
+ body?: any;
122
+
123
+ /**
124
+ * Konten kustom untuk bagian footer (biasanya tombol aksi Batal / Konfirmasi).
125
+ */
126
+ footer?: any;
127
+
128
+ /**
129
+ * Kustomisasi CSS variables atau token visual melalui UiProps.
130
+ */
131
+ ui?: ModalUiProps;
132
+
133
+ /**
134
+ * Class CSS kustom pada elemen pembungkus terluar.
135
+ */
136
+ className?: string;
137
+
138
+ /**
139
+ * Class CSS kustom (alias framework Vue).
140
+ */
141
+ class?: string;
142
+
143
+ /**
144
+ * Style inline kustom pada kontainer dialog.
145
+ */
146
+ style?: React.CSSProperties;
147
+
148
+ /**
149
+ * Class CSS kustom untuk lapisan backdrop overlay.
150
+ */
151
+ overlayClass?: string;
152
+
153
+ /**
154
+ * Style kustom untuk lapisan backdrop overlay (UiProps).
155
+ */
156
+ overlayStyle?: UiProps;
157
+
158
+ /**
159
+ * Class CSS kustom untuk kartu panel modal dialog.
160
+ */
161
+ contentClass?: string;
162
+
163
+ /**
164
+ * Style kustom untuk kartu panel modal dialog (UiProps).
165
+ */
166
+ contentStyle?: UiProps;
167
+
168
+ /**
169
+ * Class CSS kustom untuk header modal.
170
+ */
171
+ headerClass?: string;
172
+
173
+ /**
174
+ * Style kustom untuk header modal (UiProps).
175
+ */
176
+ headerStyle?: UiProps;
177
+
178
+ /**
179
+ * Class CSS kustom untuk body modal.
180
+ */
181
+ bodyClass?: string;
182
+
183
+ /**
184
+ * Style kustom untuk body modal (UiProps).
185
+ */
186
+ bodyStyle?: UiProps;
187
+
188
+ /**
189
+ * Class CSS kustom untuk footer modal.
190
+ */
191
+ footerClass?: string;
192
+
193
+ /**
194
+ * Style kustom untuk footer modal (UiProps).
195
+ */
196
+ footerStyle?: UiProps;
197
+
198
+ /**
199
+ * Callback event saat status terbuka/tertutup berubah (kompatibel dengan v-model:open pada Vue 3).
200
+ */
201
+ onUpdateOpen?: (open: boolean) => void;
202
+
203
+ /**
204
+ * Callback event saat status terbuka/tertutup berubah (alias standar React).
205
+ */
206
+ onOpenChange?: (open: boolean) => void;
207
+
208
+ /**
209
+ * Callback event saat modal terbuka.
210
+ */
211
+ onOpen?: () => void;
212
+
213
+ /**
214
+ * Callback event saat modal ditutup.
215
+ */
216
+ onClose?: () => void;
217
+
218
+ /**
219
+ * Callback event saat pengguna mencoba menutup modal tetapi dicegah karena preventClose={true} atau dismissible={false}.
220
+ */
221
+ onClosePrevent?: () => void;
222
+
223
+ /**
224
+ * Elemen pemicu (trigger), tombol, atau konten modal default.
225
+ */
226
+ children?: any;
227
+ }
228
+
229
+ /**
230
+ * Komponen Modal TTDK Design System — Selaras dengan Spesifikasi Nuxt UI Modal (UModal):
231
+ * - Mode controlled (v-model:open / open) dan uncontrolled (defaultOpen)
232
+ * - Backdrop blur overlay redup dengan penguncian scroll latar belakang otomatis (preventScroll)
233
+ * - Pilihan ukuran responsif ('xs' s/d '5xl' serta 'fullscreen')
234
+ * - Struktur slot terstandarisasi (#header, #body, #footer, #content)
235
+ * - Penanganan dismissible & preventClose (Escape key, backdrop click, callback onClosePrevent)
236
+ * - Animasi scale-in dan efek visual getar (shake) saat penutupan dicegah
237
+ */
238
+
239
+ import { buildUiVars, UiProps } from "../../core/types";
240
+
241
+ let lockCount = 0;
242
+ let originalOverflow = "";
243
+ let originalPaddingRight = "";
244
+
245
+ function lockBodyScroll() {
246
+ if (typeof document === "undefined") return;
247
+ if (lockCount === 0) {
248
+ originalOverflow = document.body.style.overflow;
249
+ originalPaddingRight = document.body.style.paddingRight;
250
+ const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
251
+ document.body.style.overflow = "hidden";
252
+ if (scrollBarWidth > 0) {
253
+ document.body.style.paddingRight = scrollBarWidth + "px";
254
+ }
255
+ }
256
+ lockCount++;
257
+ }
258
+
259
+ function unlockBodyScroll() {
260
+ if (typeof document === "undefined") return;
261
+ lockCount = Math.max(0, lockCount - 1);
262
+ if (lockCount === 0) {
263
+ document.body.style.overflow = originalOverflow;
264
+ document.body.style.paddingRight = originalPaddingRight;
265
+ }
266
+ }
267
+
268
+ function Modal(props: ModalProps) {
269
+ const [mounted, setMounted] = useState(() => false);
270
+ useEffect(() => {
271
+ setMounted(true);
272
+ }, []);
273
+ const dialogRef = useRef<HTMLDivElement>(null);
274
+ const [isOpen, setIsOpen] = useState(() =>
275
+ props.open !== undefined ? props.open : Boolean(props.defaultOpen)
276
+ );
277
+
278
+ const [isShaking, setIsShaking] = useState(() => false);
279
+
280
+ const [isAnimDone, setIsAnimDone] = useState(() => false);
281
+
282
+ const [shakeTimer, setShakeTimer] = useState<any>(null);
283
+
284
+ const [animTimer, setAnimTimer] = useState<any>(null);
285
+
286
+ function isDismissible() {
287
+ if (props.preventClose === true) return false;
288
+ return props.dismissible !== false;
289
+ }
290
+
291
+ function triggerShake() {
292
+ if (shakeTimer) {
293
+ clearTimeout(shakeTimer);
294
+ }
295
+ setIsAnimDone(true);
296
+ setIsShaking(true);
297
+ setShakeTimer(
298
+ setTimeout(() => {
299
+ setIsShaking(false);
300
+ setShakeTimer(null);
301
+ }, 400)
302
+ );
303
+ }
304
+
305
+ function openModal() {
306
+ if (!isOpen) {
307
+ setIsOpen(true);
308
+ setIsAnimDone(false);
309
+ if (animTimer) clearTimeout(animTimer);
310
+ setAnimTimer(
311
+ setTimeout(() => {
312
+ setIsAnimDone(true);
313
+ }, 250)
314
+ );
315
+ if (props.onOpen) props.onOpen();
316
+ if (props.onUpdateOpen) props.onUpdateOpen(true);
317
+ if (props.onOpenChange) props.onOpenChange(true);
318
+ }
319
+ }
320
+
321
+ function closeModal(force?: boolean) {
322
+ if (!isDismissible() && !force) {
323
+ triggerShake();
324
+ if (props.onClosePrevent) {
325
+ props.onClosePrevent();
326
+ }
327
+ return;
328
+ }
329
+ if (isOpen) {
330
+ setIsOpen(false);
331
+ setIsAnimDone(false);
332
+ if (props.onClose) props.onClose();
333
+ if (props.onUpdateOpen) props.onUpdateOpen(false);
334
+ if (props.onOpenChange) props.onOpenChange(false);
335
+ }
336
+ }
337
+
338
+ function handleOverlayClick() {
339
+ closeModal();
340
+ }
341
+
342
+ function handleKeyDown(event: any) {
343
+ if (!isOpen) return;
344
+ if (event.key === "Escape") {
345
+ closeModal();
346
+ }
347
+ }
348
+
349
+ function renderHeaderContent() {
350
+ if (props.header) {
351
+ return typeof props.header === "function"
352
+ ? props.header({
353
+ close: () => closeModal(true),
354
+ })
355
+ : props.header;
356
+ }
357
+ return null;
358
+ }
359
+
360
+ function renderBodyContent() {
361
+ if (props.body) {
362
+ return typeof props.body === "function"
363
+ ? props.body({
364
+ close: () => closeModal(true),
365
+ })
366
+ : props.body;
367
+ }
368
+ return null;
369
+ }
370
+
371
+ function renderFooterContent() {
372
+ if (props.footer) {
373
+ return typeof props.footer === "function"
374
+ ? props.footer({
375
+ close: () => closeModal(true),
376
+ })
377
+ : props.footer;
378
+ }
379
+ return null;
380
+ }
381
+
382
+ function renderCustomContent() {
383
+ if (props.content) {
384
+ return typeof props.content === "function"
385
+ ? props.content({
386
+ close: () => closeModal(true),
387
+ })
388
+ : props.content;
389
+ }
390
+ return null;
391
+ }
392
+
393
+ useEffect(() => {
394
+ if (props.open !== undefined) {
395
+ setIsOpen(props.open);
396
+ }
397
+ }, [props.open]);
398
+
399
+ useEffect(() => {
400
+ const shouldLockScroll = Boolean(props.preventScroll !== false && props.lockScroll !== false);
401
+
402
+ if (isOpen) {
403
+ if (shouldLockScroll) {
404
+ lockBodyScroll();
405
+ }
406
+
407
+ const handleEscape = (event: any) => {
408
+ if (event.key === "Escape") {
409
+ closeModal();
410
+ }
411
+ };
412
+
413
+ document.addEventListener("keydown", handleEscape, true);
414
+
415
+ return () => {
416
+ if (shouldLockScroll) {
417
+ unlockBodyScroll();
418
+ }
419
+ document.removeEventListener("keydown", handleEscape, true);
420
+ };
421
+ }
422
+ }, [isOpen, props.preventClose, props.dismissible, props.preventScroll, props.lockScroll]);
423
+
424
+ useEffect(() => {
425
+ return () => {
426
+ if (shakeTimer) {
427
+ clearTimeout(shakeTimer);
428
+ }
429
+ if (typeof document !== "undefined") {
430
+ document.removeEventListener(
431
+ "keydown",
432
+ (e: any) => handleKeyDown(e),
433
+ true
434
+ );
435
+ }
436
+ };
437
+ }, []);
438
+
439
+ if (!isOpen) return null;
440
+
441
+ const modalNode = (
442
+ <div
443
+ role="dialog"
444
+ className={`ttdk-modal-portal ${props.className || ""}`.trim()}
445
+ aria-modal={props.modal !== false ? "true" : "false"}
446
+ tabIndex={-1}
447
+ style={{
448
+ ...buildUiVars(props.ui),
449
+ ...((props as any)["style"] || {}),
450
+ ...props.style,
451
+ }}
452
+ >
453
+ {props.overlay !== false ? (
454
+ <div
455
+ className={`ttdk-modal-overlay ${
456
+ props.overlayClass || ""
457
+ }`.trim()}
458
+ style={{
459
+ ...buildUiVars(props.overlayStyle),
460
+ }}
461
+ onClick={(event) => handleOverlayClick()}
462
+ />
463
+ ) : null}
464
+ <div
465
+ ref={dialogRef}
466
+ className={`ttdk-modal-dialog ${
467
+ isAnimDone ? "ttdk-modal-anim-done" : ""
468
+ } ${isShaking ? "ttdk-modal-shake" : ""} ${
469
+ props.contentClass || ""
470
+ }`.trim()}
471
+ data-size={props.size || "md"}
472
+ data-fullscreen={props.fullscreen ? "true" : undefined}
473
+ data-scrollable={props.scrollable !== false ? "true" : undefined}
474
+ style={{
475
+ ...buildUiVars(props.contentStyle),
476
+ }}
477
+ >
478
+ {Boolean(props.content) ? <>{renderCustomContent()}</> : null}
479
+ {!props.content ? (
480
+ <>
481
+ {Boolean(
482
+ props.header || props.title || props.close !== false
483
+ ) ? (
484
+ <div
485
+ className={`ttdk-modal-header ${
486
+ props.headerClass || ""
487
+ }`.trim()}
488
+ style={{
489
+ ...buildUiVars(props.headerStyle),
490
+ }}
491
+ >
492
+ {Boolean(props.header) ? (
493
+ <>{renderHeaderContent()}</>
494
+ ) : null}
495
+ {!props.header ? (
496
+ <>
497
+ <div className="ttdk-modal-header-info">
498
+ {Boolean(props.title) ? (
499
+ <h3 className="ttdk-modal-title">{props.title}</h3>
500
+ ) : null}
501
+ {Boolean(props.description) ? (
502
+ <p className="ttdk-modal-description">
503
+ {props.description}
504
+ </p>
505
+ ) : null}
506
+ </div>
507
+ {props.close !== false ? (
508
+ <button
509
+ type="button"
510
+ className="ttdk-modal-close"
511
+ aria-label="Tutup modal"
512
+ onClick={(event) => closeModal(true)}
513
+ >
514
+ {Boolean(props.closeIcon) ? (
515
+ <>{props.closeIcon}</>
516
+ ) : null}
517
+ {!props.closeIcon ? (
518
+ <svg
519
+ width="16"
520
+ height="16"
521
+ viewBox="0 0 24 24"
522
+ fill="none"
523
+ stroke="currentColor"
524
+ >
525
+ <line x1="18" y1="6" x2="6" y2="18" />
526
+ <line x1="6" y1="6" x2="18" y2="18" />
527
+ </svg>
528
+ ) : null}
529
+ </button>
530
+ ) : null}
531
+ </>
532
+ ) : null}
533
+ </div>
534
+ ) : null}
535
+ <div
536
+ className={`ttdk-modal-body ${props.bodyClass || ""}`.trim()}
537
+ style={{
538
+ ...buildUiVars(props.bodyStyle),
539
+ }}
540
+ >
541
+ {Boolean(props.body) ? <>{renderBodyContent()}</> : null}
542
+ {!props.body && props.children ? <>{props.children}</> : null}
543
+ </div>
544
+ {Boolean(props.footer) ? (
545
+ <div
546
+ className={`ttdk-modal-footer ${
547
+ props.footerClass || ""
548
+ }`.trim()}
549
+ style={{
550
+ ...buildUiVars(props.footerStyle),
551
+ }}
552
+ >
553
+ {renderFooterContent()}
554
+ </div>
555
+ ) : null}
556
+ </>
557
+ ) : null}
558
+ </div>
559
+ </div>
560
+ );
561
+
562
+ if (mounted && typeof document !== "undefined") {
563
+ return createPortal(modalNode, document.body);
564
+ }
565
+
566
+ return modalNode;
567
+ }
568
+
569
+ export default Modal;
570
+
571
+ export { Modal };
@@ -0,0 +1,148 @@
1
+ # <Pagination /> — TTDK Component Reference
2
+
3
+ Komponen Pagination navigasi halaman dengan dukungan Compound API (ala Shadcn UI) dan Props-driven mode.
4
+
5
+ ## 1. Stylesheets & Theming (Pure CSS)
6
+ Import the corresponding stylesheet for the theme you wish to use:
7
+ - **COMMON Theme**: `import "@ttdk-design-system/library-<react|vue|svelte>/web/pagination/pagination.css";`
8
+
9
+ ## 2. Subpath Component Import
10
+ ```ts
11
+ // React
12
+ import Pagination from "@ttdk-design-system/library-react/web/pagination";
13
+
14
+ // Vue 3
15
+ import Pagination from "@ttdk-design-system/library-vue/web/pagination";
16
+
17
+ // Svelte
18
+ import Pagination from "@ttdk-design-system/library-svelte/web/pagination";
19
+ ```
20
+
21
+ ## 3. Props & API Reference
22
+
23
+ | Prop | Type | Default | Description |
24
+ | :--- | :--- | :--- | :--- |
25
+ | `page?` | `number` | `1` | Nomor halaman saat ini (1-indexed). |
26
+ | `maxPages?` | `number` | `1` | Jumlah total halaman maksimum (alias untuk totalPages). |
27
+ | `totalPages?` | `number` | `1` | Jumlah total halaman (alias maxPages). |
28
+ | `limit?` | `number` | `10` | Jumlah baris/item data per halaman (page size). |
29
+ | `onLimitChange?` | `(limit: number) => void` | `-` | Callback saat jumlah baris per halaman diubah via select box. |
30
+ | `limitOptions?` | `number[]` | `[10, 20, 50, 100]` | Pilihan angka baris per halaman pada select box. |
31
+ | `canPrevPage?` | `boolean` | `false` | Menentukan apakah navigasi ke halaman sebelumnya diizinkan (false = tombol Previous dinonaktifkan). |
32
+ | `canNextPage?` | `boolean` | `false` | Menentukan apakah navigasi ke halaman berikutnya diizinkan (false = tombol Next dinonaktifkan). |
33
+ | `onFirstPage?` | `() => void` | `-` | Callback saat tombol First Page (halaman pertama) diklik. |
34
+ | `onPrevPage?` | `() => void` | `-` | Callback saat tombol Previous Page (halaman sebelumnya) diklik. |
35
+ | `onNextPage?` | `() => void` | `-` | Callback saat tombol Next Page (halaman selanjutnya) diklik. |
36
+ | `onLastPage?` | `() => void` | `-` | Callback saat tombol Last Page (halaman terakhir) diklik. |
37
+ | `onPageChange?` | `(page: number) => void` | `-` | Callback saat nomor halaman tertentu diklik. |
38
+ | `siblingCount?` | `number` | `1` | Jumlah tetangga nomor halaman di samping kiri dan kanan halaman aktif. |
39
+ | `showEdges?` | `boolean` | `false` | Menampilkan tombol First (<<) dan Last (>>). |
40
+ | `showPageNumbers?` | `boolean` | `true` | Menampilkan nomor halaman terpaginasi (1, 2, 3...). |
41
+ | `size?` | `'sm' | 'md' | 'lg'` | `md` | Skala ukuran pagination: - `'sm'`: 28px - `'md'`: 36px (standar) - `'lg'`: 44px |
42
+ | `style?` | `any` | `-` | Inline CSS style object native. |
43
+ | `ui?` | `UiProps` | `-` | Kustomisasi visual granular root kontainer pagination (UiProps). |
44
+ | `contentStyle?` | `UiProps` | `-` | Style kustom untuk kontainer daftar pagination content (ul.ttdk-pagination-content). |
45
+ | `contentClass?` | `string` | `-` | Class CSS kustom untuk kontainer daftar pagination content. |
46
+ | `linkStyle?` | `UiProps` | `-` | Style kustom untuk tombol link nomor halaman / navigasi. |
47
+ | `linkClass?` | `string` | `-` | Class CSS kustom untuk tombol link nomor halaman / navigasi. |
48
+ | `activeLinkStyle?` | `UiProps` | `-` | Style kustom untuk tombol nomor halaman saat aktif. |
49
+ | `activeLinkClass?` | `string` | `-` | Class CSS kustom untuk tombol nomor halaman saat aktif. |
50
+ | `limitStyle?` | `UiProps` | `-` | Style kustom untuk kontainer baris per halaman (limit). |
51
+ | `limitClass?` | `string` | `-` | Class CSS kustom untuk kontainer baris per halaman (limit). |
52
+ | `limitSelectStyle?` | `UiProps` | `-` | Style kustom untuk elemen Selectbox baris per halaman (limit). |
53
+ | `limitSlot?` | `any` | `-` | Slot kustom untuk menggantikan elemen limit selector bawaan. |
54
+ | `showLimit?` | `boolean` | `-` | Menentukan apakah kontrol baris per halaman (limit selectbox) ditampilkan. Jika tidak disetel, otomatis mengikuti keberadaan callback onLimitChange. |
55
+ | `showLimitLabel?` | `boolean` | `true` | Menentukan apakah teks label 'Baris per halaman:' ditampilkan di samping selectbox limit. |
56
+ | `limitLabel?` | `string` | `Baris per halaman:` | Teks label untuk opsi baris per halaman. |
57
+ | `firstIcon?` | `any` | `-` | Ikon kustom untuk tombol halaman pertama (first page, menggantikan <<). |
58
+ | `prevIcon?` | `any` | `-` | Ikon kustom untuk tombol halaman sebelumnya (previous page, menggantikan <). |
59
+ | `nextIcon?` | `any` | `-` | Ikon kustom untuk tombol halaman selanjutnya (next page, menggantikan >). |
60
+ | `lastIcon?` | `any` | `-` | Ikon kustom untuk tombol halaman terakhir (last page, menggantikan >>). |
61
+ | `ellipsisIcon?` | `any` | `-` | Ikon kustom untuk separator elipsis (menggantikan ...). |
62
+ | `prevText?` | `string` | `Previous` | Label teks tombol Previous. |
63
+ | `nextText?` | `string` | `Next` | Label teks tombol Next. |
64
+ | `iconOnly?` | `boolean` | `false` | Mode hanya menampilkan ikon pada tombol Previous dan Next tanpa teks label. |
65
+ | `className?` | `string` | `-` | Class CSS kustom. |
66
+ | `children?` | `any` | `-` | Konten kustom atau subkomponen compound (PaginationContent, PaginationItem, dll). |
67
+
68
+ ### Sub-interface: `PaginationPageItem`
69
+
70
+ | Field | Type | Default | Description |
71
+ | :--- | :--- | :--- | :--- |
72
+ | `type` | `'page' | 'ellipsis'` | `-` | |
73
+ | `value` | `number | string` | `-` | |
74
+ | `key` | `string` | `-` | |
75
+
76
+ ## 4. Usage Examples
77
+
78
+ ### React Example
79
+ ```tsx
80
+ import { useState } from "react";
81
+ import Pagination from "@ttdk-design-system/library-react/pagination";
82
+ import "@ttdk-design-system/library-react/pagination/pagination.css";
83
+
84
+ export function Example() {
85
+ const [page, setPage] = useState(1);
86
+ const [limit, setLimit] = useState(10);
87
+
88
+ return (
89
+ <Pagination
90
+ page={page}
91
+ totalPages={15}
92
+ limit={limit}
93
+ limitOptions={[10, 25, 50, 100]}
94
+ canPrevPage={page > 1}
95
+ canNextPage={page < 15}
96
+ onPageChange={(p) => setPage(p)}
97
+ onLimitChange={(l) => setLimit(l)}
98
+ />
99
+ );
100
+ }
101
+ ```
102
+
103
+ ### Vue 3 Example
104
+ ```vue
105
+ <script setup lang="ts">
106
+ import { ref } from "vue";
107
+ import Pagination from "@ttdk-design-system/library-vue/pagination";
108
+ import "@ttdk-design-system/library-vue/pagination/pagination.css";
109
+
110
+ const page = ref(1);
111
+ const limit = ref(10);
112
+ </script>
113
+
114
+ <template>
115
+ <Pagination
116
+ :page="page"
117
+ :total-pages="15"
118
+ :limit="limit"
119
+ :limit-options="[10, 25, 50, 100]"
120
+ :can-prev-page="page > 1"
121
+ :can-next-page="page < 15"
122
+ @page-change="(p: number) => (page = p)"
123
+ @limit-change="(l: number) => (limit = l)"
124
+ />
125
+ </template>
126
+ ```
127
+
128
+ ### Svelte Example
129
+ ```svelte
130
+ <script lang="ts">
131
+ import Pagination from "@ttdk-design-system/library-svelte/pagination";
132
+ import "@ttdk-design-system/library-svelte/pagination/pagination.css";
133
+
134
+ let page = 1;
135
+ let limit = 10;
136
+ </script>
137
+
138
+ <Pagination
139
+ {page}
140
+ totalPages={15}
141
+ {limit}
142
+ limitOptions={[10, 25, 50, 100]}
143
+ canPrevPage={page > 1}
144
+ canNextPage={page < 15}
145
+ on:pageChange={(e) => (page = e.detail)}
146
+ on:limitChange={(e) => (limit = e.detail)}
147
+ />
148
+ ```