@keenthemes/ktui 1.0.10 → 1.0.12

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 (139) hide show
  1. package/README.md +2 -2
  2. package/dist/ktui.js +1283 -1100
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/examples/select/basic-usage.html +43 -0
  6. package/examples/select/combobox-icons.html +58 -0
  7. package/examples/select/combobox.html +46 -0
  8. package/examples/select/description.html +69 -0
  9. package/examples/select/disable-option.html +43 -0
  10. package/examples/select/disable-select.html +34 -0
  11. package/examples/select/icon-description.html +56 -0
  12. package/examples/select/icon-multiple.html +58 -0
  13. package/examples/select/icon.html +58 -0
  14. package/examples/select/max-selection.html +39 -0
  15. package/examples/select/modal.html +70 -0
  16. package/examples/select/multiple.html +42 -0
  17. package/examples/select/placeholder.html +43 -0
  18. package/examples/select/remote-data.html +32 -0
  19. package/examples/select/search.html +49 -0
  20. package/examples/select/tags-icons.html +58 -0
  21. package/examples/select/tags-selected.html +59 -0
  22. package/examples/select/tags.html +58 -0
  23. package/examples/select/template-customization.html +65 -0
  24. package/examples/select/test.html +94 -0
  25. package/examples/toast/example.html +427 -0
  26. package/lib/cjs/components/component.js +1 -1
  27. package/lib/cjs/components/component.js.map +1 -1
  28. package/lib/cjs/components/datatable/datatable.js +22 -6
  29. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  30. package/lib/cjs/components/modal/modal.js +0 -4
  31. package/lib/cjs/components/modal/modal.js.map +1 -1
  32. package/lib/cjs/components/select/combobox.js +38 -120
  33. package/lib/cjs/components/select/combobox.js.map +1 -1
  34. package/lib/cjs/components/select/config.js +4 -16
  35. package/lib/cjs/components/select/config.js.map +1 -1
  36. package/lib/cjs/components/select/dropdown.js +10 -49
  37. package/lib/cjs/components/select/dropdown.js.map +1 -1
  38. package/lib/cjs/components/select/index.js +2 -1
  39. package/lib/cjs/components/select/index.js.map +1 -1
  40. package/lib/cjs/components/select/option.js +21 -4
  41. package/lib/cjs/components/select/option.js.map +1 -1
  42. package/lib/cjs/components/select/remote.js +1 -37
  43. package/lib/cjs/components/select/remote.js.map +1 -1
  44. package/lib/cjs/components/select/search.js +11 -41
  45. package/lib/cjs/components/select/search.js.map +1 -1
  46. package/lib/cjs/components/select/select.js +213 -326
  47. package/lib/cjs/components/select/select.js.map +1 -1
  48. package/lib/cjs/components/select/tags.js +39 -31
  49. package/lib/cjs/components/select/tags.js.map +1 -1
  50. package/lib/cjs/components/select/templates.js +120 -179
  51. package/lib/cjs/components/select/templates.js.map +1 -1
  52. package/lib/cjs/components/select/types.js +0 -12
  53. package/lib/cjs/components/select/types.js.map +1 -1
  54. package/lib/cjs/components/select/utils.js +204 -257
  55. package/lib/cjs/components/select/utils.js.map +1 -1
  56. package/lib/cjs/components/toast/index.js +10 -0
  57. package/lib/cjs/components/toast/index.js.map +1 -0
  58. package/lib/cjs/components/toast/toast.js +543 -0
  59. package/lib/cjs/components/toast/toast.js.map +1 -0
  60. package/lib/cjs/components/toast/types.js +7 -0
  61. package/lib/cjs/components/toast/types.js.map +1 -0
  62. package/lib/cjs/helpers/dom.js +24 -0
  63. package/lib/cjs/helpers/dom.js.map +1 -1
  64. package/lib/cjs/index.js +5 -1
  65. package/lib/cjs/index.js.map +1 -1
  66. package/lib/esm/components/component.js +1 -1
  67. package/lib/esm/components/component.js.map +1 -1
  68. package/lib/esm/components/datatable/datatable.js +22 -6
  69. package/lib/esm/components/datatable/datatable.js.map +1 -1
  70. package/lib/esm/components/modal/modal.js +0 -4
  71. package/lib/esm/components/modal/modal.js.map +1 -1
  72. package/lib/esm/components/select/combobox.js +39 -121
  73. package/lib/esm/components/select/combobox.js.map +1 -1
  74. package/lib/esm/components/select/config.js +3 -15
  75. package/lib/esm/components/select/config.js.map +1 -1
  76. package/lib/esm/components/select/dropdown.js +10 -49
  77. package/lib/esm/components/select/dropdown.js.map +1 -1
  78. package/lib/esm/components/select/index.js +1 -1
  79. package/lib/esm/components/select/index.js.map +1 -1
  80. package/lib/esm/components/select/option.js +21 -4
  81. package/lib/esm/components/select/option.js.map +1 -1
  82. package/lib/esm/components/select/remote.js +1 -37
  83. package/lib/esm/components/select/remote.js.map +1 -1
  84. package/lib/esm/components/select/search.js +12 -42
  85. package/lib/esm/components/select/search.js.map +1 -1
  86. package/lib/esm/components/select/select.js +214 -327
  87. package/lib/esm/components/select/select.js.map +1 -1
  88. package/lib/esm/components/select/tags.js +39 -31
  89. package/lib/esm/components/select/tags.js.map +1 -1
  90. package/lib/esm/components/select/templates.js +119 -178
  91. package/lib/esm/components/select/templates.js.map +1 -1
  92. package/lib/esm/components/select/types.js +1 -11
  93. package/lib/esm/components/select/types.js.map +1 -1
  94. package/lib/esm/components/select/utils.js +201 -255
  95. package/lib/esm/components/select/utils.js.map +1 -1
  96. package/lib/esm/components/toast/index.js +6 -0
  97. package/lib/esm/components/toast/index.js.map +1 -0
  98. package/lib/esm/components/toast/toast.js +540 -0
  99. package/lib/esm/components/toast/toast.js.map +1 -0
  100. package/lib/esm/components/toast/types.js +6 -0
  101. package/lib/esm/components/toast/types.js.map +1 -0
  102. package/lib/esm/helpers/dom.js +24 -0
  103. package/lib/esm/helpers/dom.js.map +1 -1
  104. package/lib/esm/index.js +3 -0
  105. package/lib/esm/index.js.map +1 -1
  106. package/package.json +8 -6
  107. package/src/components/alert/alert.css +20 -2
  108. package/src/components/badge/badge.css +5 -0
  109. package/src/components/component.ts +4 -0
  110. package/src/components/datatable/datatable.ts +24 -16
  111. package/src/components/drawer/drawer.css +1 -1
  112. package/src/components/input/input.css +3 -1
  113. package/src/components/link/link.css +2 -2
  114. package/src/components/modal/modal.css +18 -2
  115. package/src/components/modal/modal.ts +0 -5
  116. package/src/components/select/combobox.ts +42 -149
  117. package/src/components/select/config.ts +38 -33
  118. package/src/components/select/dropdown.ts +8 -55
  119. package/src/components/select/index.ts +1 -1
  120. package/src/components/select/option.ts +28 -7
  121. package/src/components/select/remote.ts +2 -42
  122. package/src/components/select/search.ts +14 -54
  123. package/src/components/select/select.css +49 -0
  124. package/src/components/select/select.ts +231 -437
  125. package/src/components/select/tags.ts +40 -37
  126. package/src/components/select/templates.ts +166 -303
  127. package/src/components/select/types.ts +0 -10
  128. package/src/components/select/utils.ts +214 -304
  129. package/src/components/table/table.css +1 -1
  130. package/src/components/textarea/textarea.css +2 -1
  131. package/src/components/toast/index.ts +7 -0
  132. package/src/components/toast/toast.css +60 -0
  133. package/src/components/toast/toast.ts +605 -0
  134. package/src/components/toast/types.ts +169 -0
  135. package/src/helpers/dom.ts +30 -0
  136. package/src/index.ts +4 -0
  137. package/styles/main.css +3 -0
  138. package/styles/vars.css +138 -0
  139. package/styles.css +1 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ /* Base Styles */
7
+ @layer components {
8
+ /* Container */
9
+ .kt-toast-container {
10
+ @apply fixed;
11
+ }
12
+
13
+ /* Toast */
14
+ .kt-toast {
15
+ @apply fixed z-9999 max-w-[95%] w-76 shadow-sm pointer-events-auto overflow-hidden;
16
+
17
+ opacity: 0;
18
+ animation: kt-toast-in 0.28s cubic-bezier(.4,0,.2,1) forwards;
19
+ transition: top 0.28s cubic-bezier(.4,0,.2,1), opacity 0.28s cubic-bezier(.4,0,.2,1);
20
+
21
+ &.kt-toast-top-end { @apply bottom-auto top-0 end-0; }
22
+ &.kt-toast-top-center { @apply bottom-auto top-0 start-1/2 -translate-x-1/2; }
23
+ &.kt-toast-top-start { @apply bottom-auto top-0 start-0; }
24
+ &.kt-toast-bottom-end { @apply top-auto bottom-0 end-0; }
25
+ &.kt-toast-bottom-center { @apply top-auto bottom-0 start-1/2 -translate-x-1/2; }
26
+ &.kt-toast-bottom-start { @apply top-auto bottom-0 start-0; }
27
+ }
28
+
29
+ /* Progress */
30
+ .kt-toast-progress {
31
+ @apply fixed start-0 bottom-0 w-full h-[3px] bg-primary;
32
+ transform-origin: left;
33
+ animation: kt-toast-progress-line linear forwards;
34
+ }
35
+ }
36
+
37
+ /* RTL Styles */
38
+ @layer components {
39
+ [dir='rtl'] .kt-toast-progress {
40
+ transform-origin: right;
41
+ }
42
+ }
43
+
44
+ /* Animations */
45
+ @layer components {
46
+ @keyframes kt-toast-in {
47
+ from { opacity: 0; transform: translateY(-24px); }
48
+ to { opacity: 1; transform: translateY(0); }
49
+ }
50
+
51
+ @keyframes kt-toast-out {
52
+ from { opacity: 1; }
53
+ to { opacity: 0; }
54
+ }
55
+
56
+ @keyframes kt-toast-progress-line {
57
+ from { transform: scaleX(1); }
58
+ to { transform: scaleX(0); }
59
+ }
60
+ }
@@ -0,0 +1,605 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ import KTComponent from '../component';
7
+ import KTData from '../../helpers/data';
8
+ import {
9
+ KTToastOptions,
10
+ KTToastConfig,
11
+ KTToastInstance,
12
+ KTToastPosition,
13
+ } from './types';
14
+
15
+ const DEFAULT_CONFIG: KTToastConfig = {
16
+ position: 'top-end',
17
+ duration: 4000,
18
+ className: '',
19
+ maxToasts: 5,
20
+ offset: 15,
21
+ gap: 10,
22
+ };
23
+
24
+ const DEFAULT_TOAST_OPTIONS: KTToastOptions = {
25
+ appearance: 'solid',
26
+ progress: false,
27
+ size: 'md',
28
+ action: false,
29
+ cancel: false,
30
+ dismiss: true,
31
+ };
32
+
33
+ import type { KTToastConfigInterface, KTToastInterface } from './types';
34
+
35
+ export class KTToast extends KTComponent implements KTToastInterface {
36
+ protected override _name: string = 'toast';
37
+ protected override _defaultConfig: KTToastConfigInterface = DEFAULT_CONFIG;
38
+ protected override _config: KTToastConfigInterface = DEFAULT_CONFIG;
39
+ protected _defaultToastOptions: KTToastOptions = DEFAULT_TOAST_OPTIONS;
40
+ private static containerMap: Map<KTToastPosition, HTMLElement> = new Map();
41
+ private static toasts: Map<string, KTToastInstance> = new Map();
42
+ private static globalConfig: KTToastConfig = { ...DEFAULT_CONFIG };
43
+
44
+ /**
45
+ * Creates a new KTToast instance for a specific element (not commonly used; most use static API).
46
+ * @param element The target HTML element.
47
+ * @param config Optional toast config for this instance.
48
+ */
49
+ constructor(element: HTMLElement, config?: Partial<KTToastConfigInterface>) {
50
+ super();
51
+ if (KTData.has(element, this._name)) return;
52
+ this._init(element);
53
+ this._buildConfig(config);
54
+ KTData.set(element, this._name, this);
55
+ }
56
+
57
+ /**
58
+ * Generates the HTML content for a toast based on the provided options.
59
+ * @param options Toast options (message, icon, actions, etc).
60
+ * @returns The toast's HTML markup as a string.
61
+ */
62
+ static getContent(options?: KTToastOptions) {
63
+ const classNames = {
64
+ ...((this.globalConfig.classNames as any) || {}),
65
+ ...((options?.classNames as any) || {}),
66
+ };
67
+
68
+ if (options?.content) {
69
+ if (typeof options.content === 'string') {
70
+ return options.content;
71
+ } else if (typeof options.content === 'function') {
72
+ const node = options.content();
73
+ if (node instanceof HTMLElement) {
74
+ return node.outerHTML;
75
+ }
76
+ } else if (options.content instanceof HTMLElement) {
77
+ return options.content.outerHTML;
78
+ }
79
+ }
80
+
81
+ let template = '';
82
+
83
+ if (options?.icon) {
84
+ template +=
85
+ '<div class="kt-alert-icon ' +
86
+ (classNames.icon || '') +
87
+ '">' +
88
+ options.icon +
89
+ '</div>';
90
+ }
91
+
92
+ if (options?.message) {
93
+ template +=
94
+ '<div class="kt-alert-title ' +
95
+ (classNames.message || '') +
96
+ '">' +
97
+ options.message +
98
+ '</div>';
99
+ }
100
+
101
+ if (
102
+ options?.action !== false ||
103
+ options?.dismiss !== false ||
104
+ options?.cancel !== false
105
+ ) {
106
+ template +=
107
+ '<div class="kt-alert-toolbar ' + (classNames.toolbar || '') + '">';
108
+ template +=
109
+ '<div class="kt-alert-actions ' + (classNames.actions || '') + '">';
110
+
111
+ if (options?.action && typeof options.action === 'object') {
112
+ template +=
113
+ '<button data-kt-toast-action="true" class="' +
114
+ (options.action.className || '') +
115
+ '">' +
116
+ options.action.label +
117
+ '</button>';
118
+ }
119
+
120
+ if (options?.cancel && typeof options.cancel === 'object') {
121
+ template +=
122
+ '<button data-kt-toast-cancel="true" class="' +
123
+ (options.cancel.className || '') +
124
+ '">' +
125
+ options.cancel.label +
126
+ '</button>';
127
+ }
128
+
129
+ if (options?.dismiss !== false) {
130
+ template +=
131
+ '<button data-kt-toast-dismiss="true" class="kt-alert-close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>';
132
+ }
133
+
134
+ template += '</div>';
135
+ template += '</div>';
136
+ }
137
+
138
+ template += '</div>';
139
+
140
+ return template;
141
+ }
142
+
143
+ /**
144
+ * Update all toasts in the container with smooth animation.
145
+ *
146
+ * @param container The toast container element.
147
+ * @param offset Optional offset from the edge.
148
+ */
149
+ static update(container: HTMLElement | null, offset?: number) {
150
+ if (!container) return;
151
+ offset =
152
+ typeof offset === 'number' ? offset : (this.globalConfig.offset ?? 15);
153
+ requestAnimationFrame(() => {
154
+ const gap = this.globalConfig.gap ?? 8;
155
+ // Group toasts by alignment (top/bottom)
156
+ const positionGroups: Record<string, HTMLElement[]> = {
157
+ top: [],
158
+ bottom: [],
159
+ };
160
+ const toasts = Array.from(container.children) as HTMLElement[];
161
+ toasts.forEach((toast) => {
162
+ if (
163
+ toast.classList.contains('kt-toast-top-end') ||
164
+ toast.classList.contains('kt-toast-top-center') ||
165
+ toast.classList.contains('kt-toast-top-start')
166
+ ) {
167
+ positionGroups.top.push(toast);
168
+ } else {
169
+ positionGroups.bottom.push(toast);
170
+ }
171
+ });
172
+
173
+ // Stack top toasts from the top down
174
+ let currentOffset = offset;
175
+ positionGroups.top.forEach((toast) => {
176
+ toast.style.top = `${currentOffset}px`;
177
+ toast.style.bottom = '';
178
+ toast.style.transition =
179
+ 'top 0.28s cubic-bezier(.4,0,.2,1), opacity 0.28s cubic-bezier(.4,0,.2,1)';
180
+ currentOffset += toast.offsetHeight + gap;
181
+
182
+ if (toast.classList.contains('kt-toast-top-start')) {
183
+ toast.style.insetInlineStart = `${offset}px`;
184
+ }
185
+
186
+ if (toast.classList.contains('kt-toast-top-end')) {
187
+ toast.style.insetInlineEnd = `${offset}px`;
188
+ }
189
+ });
190
+
191
+ // Stack bottom toasts from the bottom up
192
+ currentOffset = offset;
193
+ for (let i = positionGroups.bottom.length - 1; i >= 0; i--) {
194
+ const toast = positionGroups.bottom[i];
195
+ toast.style.bottom = `${currentOffset}px`;
196
+ toast.style.top = '';
197
+ toast.style.transition =
198
+ 'bottom 0.28s cubic-bezier(.4,0,.2,1), opacity 0.28s cubic-bezier(.4,0,.2,1)';
199
+ currentOffset += toast.offsetHeight + gap;
200
+
201
+ if (toast.classList.contains('kt-toast-bottom-start')) {
202
+ toast.style.insetInlineStart = `${offset}px`;
203
+ }
204
+
205
+ if (toast.classList.contains('kt-toast-bottom-end')) {
206
+ toast.style.insetInlineEnd = `${offset}px`;
207
+ }
208
+ }
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Set global toast configuration options.
214
+ * @param options Partial toast config to merge with global config.
215
+ */
216
+ static config(options: Partial<KTToastConfig>) {
217
+ this.globalConfig = { ...this.globalConfig, ...options };
218
+ }
219
+
220
+ /**
221
+ * Show a toast notification.
222
+ * @param inputOptions Toast options (message, duration, variant, etc).
223
+ * @returns Toast instance with dismiss method, or undefined if invalid input.
224
+ */
225
+ static show(
226
+ inputOptions?: KTToastOptions,
227
+ ): (KTToastInstance & { dismiss: () => void }) | undefined {
228
+ const options = { ...DEFAULT_TOAST_OPTIONS, ...inputOptions };
229
+
230
+ if (!options || (!options.message && !options.content)) {
231
+ return undefined;
232
+ }
233
+
234
+ // Always resolve the id once and use it everywhere
235
+ const id = `kt-toast-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
236
+
237
+ const position =
238
+ options.position || this.globalConfig.position || 'top-end';
239
+
240
+ const classNames = {
241
+ ...((this.globalConfig.classNames as any) || {}),
242
+ ...((options.classNames as any) || {}),
243
+ };
244
+
245
+ let container = this.containerMap.get(position);
246
+
247
+ if (!container) {
248
+ container = document.createElement('div');
249
+ const classNames = {
250
+ ...((this.globalConfig.classNames as any) || {}),
251
+ ...((options.classNames as any) || {}),
252
+ };
253
+ // Fallback to default hardcoded classes if not provided in options or globalConfig
254
+ container.className =
255
+ classNames.container || `kt-toast-container ${position}`;
256
+ document.body.appendChild(container);
257
+ this.containerMap.set(position, container);
258
+ }
259
+
260
+ // Enforce maxToasts
261
+ if (
262
+ container.children.length >=
263
+ (this.globalConfig.maxToasts || DEFAULT_CONFIG.maxToasts)
264
+ ) {
265
+ const firstToast = container.firstElementChild;
266
+ if (firstToast) {
267
+ firstToast.classList.add('kt-toast-closing');
268
+ firstToast.addEventListener('animationend', () => {
269
+ firstToast.remove();
270
+ });
271
+ }
272
+ }
273
+
274
+ // Create toast element
275
+ const variantMap = {
276
+ info: 'kt-alert-info',
277
+ success: 'kt-alert-success',
278
+ error: 'kt-alert-error',
279
+ warning: 'kt-alert-warning',
280
+ primary: 'kt-alert-primary',
281
+ secondary: 'kt-alert-secondary',
282
+ destructive: 'kt-alert-destructive',
283
+ mono: 'kt-alert-mono',
284
+ };
285
+
286
+ const appearanceMap = {
287
+ solid: 'kt-alert-solid',
288
+ outline: 'kt-alert-outline',
289
+ light: 'kt-alert-light',
290
+ };
291
+
292
+ const sizeMap = {
293
+ sm: 'kt-alert-sm',
294
+ md: 'kt-alert-md',
295
+ lg: 'kt-alert-lg',
296
+ };
297
+
298
+ const toast = document.createElement('div');
299
+ toast.className = `kt-toast kt-alert ${variantMap[options.variant] || ''} ${appearanceMap[options.appearance] || ''} ${sizeMap[options.size] || ''} ${options.className || ''} ${classNames.toast || ''}`;
300
+ // ARIA support
301
+ toast.setAttribute('role', options.role || 'status');
302
+ toast.setAttribute('aria-live', 'polite');
303
+ toast.setAttribute('aria-atomic', 'true');
304
+ toast.setAttribute('tabindex', '0');
305
+
306
+ // Always resolve the id once and use it everywhere
307
+ // Always resolve id ONCE at the top, use everywhere
308
+ // (Move this up to replace the previous const id = ... assignment)
309
+
310
+ // Populate content via getContent
311
+ const contentHtml = KTToast.getContent(options);
312
+ toast.innerHTML = contentHtml;
313
+
314
+ // Assign event handlers to buttons by data attribute
315
+ const actionBtn = toast.querySelector(
316
+ '[data-kt-toast-action]',
317
+ ) as HTMLButtonElement | null;
318
+
319
+ if (
320
+ actionBtn &&
321
+ options.action &&
322
+ typeof options.action === 'object' &&
323
+ options.action.onClick
324
+ ) {
325
+ actionBtn.addEventListener('click', (e) => {
326
+ e.stopPropagation();
327
+ if (typeof options.action === 'object' && options.action.onClick) {
328
+ options.action.onClick(id);
329
+ KTToast.close(id);
330
+ }
331
+ });
332
+ }
333
+
334
+ const cancelBtn = toast.querySelector(
335
+ '[data-kt-toast-cancel]',
336
+ ) as HTMLButtonElement | null;
337
+
338
+ if (cancelBtn && options.cancel && typeof options.cancel === 'object') {
339
+ cancelBtn.addEventListener('click', (e) => {
340
+ e.stopPropagation();
341
+ if (typeof options.cancel === 'object' && options.cancel.onClick) {
342
+ options.cancel.onClick(id);
343
+ KTToast.close(id);
344
+ }
345
+ });
346
+ }
347
+
348
+ // Dismiss button handler
349
+ const dismissBtn = toast.querySelector(
350
+ '[data-kt-toast-dismiss]',
351
+ ) as HTMLButtonElement | null;
352
+
353
+ if (dismissBtn && options.dismiss !== false) {
354
+ dismissBtn.addEventListener('click', (e) => {
355
+ e.stopPropagation();
356
+ KTToast.close(id);
357
+ });
358
+ }
359
+
360
+ // If modal-like, set aria-modal
361
+ if (options.important) toast.setAttribute('aria-modal', 'true');
362
+ toast.style.pointerEvents = 'auto';
363
+
364
+ // Progress line
365
+ const duration = options.important
366
+ ? null
367
+ : (options.duration ??
368
+ this.globalConfig.duration ??
369
+ DEFAULT_CONFIG.duration);
370
+
371
+ if (duration && options.progress) {
372
+ const progress = document.createElement('div');
373
+ progress.className = 'kt-toast-progress ' + (classNames.progress || '');
374
+ progress.style.animationDuration = duration + 'ms';
375
+ progress.setAttribute('data-kt-toast-progress', 'true');
376
+ toast.appendChild(progress);
377
+ }
378
+
379
+ // Assign direction class to the toast itself, not the container
380
+ const directionClassMap: Record<string, string> = {
381
+ 'top-end': 'kt-toast-top-end',
382
+ 'top-center': 'kt-toast-top-center',
383
+ 'top-start': 'kt-toast-top-start',
384
+ 'bottom-end': 'kt-toast-bottom-end',
385
+ 'bottom-center': 'kt-toast-bottom-center',
386
+ 'bottom-start': 'kt-toast-bottom-start',
387
+ };
388
+ Object.values(directionClassMap).forEach((cls) =>
389
+ toast.classList.remove(cls),
390
+ );
391
+ const dirClass = directionClassMap[position] || 'kt-toast-top-end';
392
+ toast.classList.add(dirClass);
393
+
394
+ // Enforce maxToasts: remove oldest if needed
395
+ const maxToasts =
396
+ options.maxToasts ??
397
+ this.globalConfig.maxToasts ??
398
+ DEFAULT_CONFIG.maxToasts;
399
+ const currentToasts = Array.from(container.children) as HTMLElement[];
400
+ if (currentToasts.length >= maxToasts && currentToasts.length > 0) {
401
+ const oldestToast = currentToasts[currentToasts.length - 1];
402
+ const oldestId = oldestToast.getAttribute('data-kt-toast-id');
403
+ if (oldestId) {
404
+ KTToast.close(oldestId);
405
+ } else {
406
+ oldestToast.remove();
407
+ }
408
+ }
409
+
410
+ // Insert toast at the top
411
+ container.insertBefore(toast, container.firstChild);
412
+ KTToast.update(container);
413
+
414
+ // Play beep if requested
415
+ if (options.beep) {
416
+ try {
417
+ // Use Web Audio API for a short beep
418
+ const ctx = new (window.AudioContext ||
419
+ (window as any).webkitAudioContext)();
420
+ const o = ctx.createOscillator();
421
+ const g = ctx.createGain();
422
+ o.type = 'sine';
423
+ o.frequency.value = 880;
424
+ g.gain.value = 0.09;
425
+ o.connect(g);
426
+ g.connect(ctx.destination);
427
+ o.start();
428
+ setTimeout(() => {
429
+ o.stop();
430
+ ctx.close();
431
+ }, 120);
432
+ } catch (e) {
433
+ /* ignore */
434
+ }
435
+ }
436
+
437
+ KTToast._fireEventOnElement(toast, 'show', { id });
438
+ KTToast._dispatchEventOnElement(toast, 'show', { id });
439
+ const instance: KTToastInstance = { id, element: toast, timeoutId: 0 };
440
+ KTToast.toasts.set(id, instance);
441
+
442
+ // Auto-dismiss
443
+ let timeoutId: number | undefined = undefined;
444
+ let remaining = duration;
445
+ let startTime: number | undefined;
446
+ let paused = false;
447
+ let progressEl: HTMLElement | null = null;
448
+ if (duration) {
449
+ const startTimer = (ms: number) => {
450
+ startTime = Date.now();
451
+ timeoutId = window.setTimeout(() => {
452
+ options.onAutoClose?.(id);
453
+ KTToast.close(id);
454
+ }, ms);
455
+ instance.timeoutId = timeoutId;
456
+ };
457
+ startTimer(duration);
458
+
459
+ if (options.pauseOnHover) {
460
+ progressEl = toast.querySelector('[data-kt-toast-progress]');
461
+ let progressPausedAt = 0;
462
+ const pause = () => {
463
+ if (!paused && timeoutId) {
464
+ paused = true;
465
+ window.clearTimeout(timeoutId);
466
+ if (startTime) {
467
+ remaining -= Date.now() - startTime;
468
+ }
469
+ // Pause progress bar
470
+ if (progressEl) {
471
+ const computedStyle = window.getComputedStyle(progressEl);
472
+ const matrix = computedStyle.transform;
473
+ let scaleX = 1;
474
+ if (matrix && matrix !== 'none') {
475
+ const values = matrix.match(/matrix\(([^)]+)\)/);
476
+ if (values && values[1]) {
477
+ scaleX = parseFloat(values[1].split(',')[0]);
478
+ }
479
+ }
480
+ progressPausedAt = scaleX;
481
+ progressEl.style.animation = 'none';
482
+ progressEl.style.transition = 'none';
483
+ progressEl.style.transform = `scaleX(${scaleX})`;
484
+ }
485
+ }
486
+ };
487
+ const resume = () => {
488
+ if (paused && remaining > 0) {
489
+ paused = false;
490
+ startTimer(remaining);
491
+ // Resume progress bar
492
+ if (progressEl) {
493
+ progressEl.style.transition = 'transform 0ms';
494
+ progressEl.style.transform = `scaleX(${progressPausedAt})`;
495
+ progressEl.offsetHeight; // force reflow
496
+ progressEl.style.transition = `transform ${remaining}ms linear`;
497
+ progressEl.style.transform = 'scaleX(0)';
498
+ }
499
+ }
500
+ };
501
+ toast.addEventListener('mouseenter', pause);
502
+ toast.addEventListener('mouseleave', resume);
503
+ }
504
+ }
505
+
506
+ KTToast._fireEventOnElement(toast, 'shown', { id });
507
+ KTToast._dispatchEventOnElement(toast, 'shown', { id });
508
+
509
+ return {
510
+ ...instance,
511
+ dismiss: () => KTToast.close(id),
512
+ };
513
+ }
514
+
515
+ /**
516
+ * Close and remove all active toasts.
517
+ */
518
+ static clearAll() {
519
+ for (const id of Array.from(this.toasts.keys())) {
520
+ console.log('clearAll:', id);
521
+ this.close(id);
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Close a toast by ID or instance.
527
+ * @param idOrInstance Toast ID string or KTToastInstance.
528
+ */
529
+ static close(idOrInstance?: string | KTToastInstance) {
530
+ let inst: (KTToastInstance & { _closing?: boolean }) | undefined;
531
+ let id: string | undefined;
532
+ if (!idOrInstance) return;
533
+ if (typeof idOrInstance === 'string') {
534
+ id = idOrInstance;
535
+ inst = this.toasts.get(id);
536
+ } else if (typeof idOrInstance === 'object' && idOrInstance.id) {
537
+ id = idOrInstance.id;
538
+ inst = idOrInstance as KTToastInstance & { _closing?: boolean };
539
+ }
540
+
541
+ if (!inst || !id) return;
542
+ if (inst._closing) return; // Prevent double-close
543
+ inst._closing = true;
544
+
545
+ clearTimeout(inst.timeoutId);
546
+
547
+ KTToast._fireEventOnElement(inst.element, 'hide', { id });
548
+ KTToast._dispatchEventOnElement(inst.element, 'hide', { id });
549
+ // Remove progress bar instantly if present
550
+ const progressEl = inst.element.querySelector('[data-kt-toast-progress]');
551
+ if (progressEl) progressEl.remove();
552
+ inst.element.style.animation = 'kt-toast-out 0.25s forwards';
553
+
554
+ setTimeout(() => {
555
+ const parent = inst?.element.parentElement as HTMLElement | null;
556
+ inst?.element.remove();
557
+ KTToast.toasts.delete(id!);
558
+ // Try to call onDismiss if available in the toast instance (if stored)
559
+ if (typeof (inst as any).options?.onDismiss === 'function') {
560
+ (inst as any).options.onDismiss(id);
561
+ }
562
+ KTToast._fireEventOnElement(inst.element, 'hidden', { id });
563
+ KTToast._dispatchEventOnElement(inst.element, 'hidden', { id });
564
+ // update toasts asynchronously after DOM update
565
+ setTimeout(() => {
566
+ KTToast.update(parent);
567
+ }, 0);
568
+ }, 250);
569
+ }
570
+
571
+ /**
572
+ * Dispatches a custom 'kt.toast.{eventType}' event on the given element.
573
+ * @param element The toast element.
574
+ * @param eventType The event type (e.g. 'show', 'hide').
575
+ * @param payload Optional event detail payload.
576
+ */
577
+ private static _fireEventOnElement(
578
+ element: HTMLElement,
579
+ eventType: string,
580
+ payload?: object,
581
+ ) {
582
+ const event = new CustomEvent(`kt.toast.${eventType}`, { detail: payload });
583
+ element.dispatchEvent(event);
584
+ }
585
+
586
+ /**
587
+ * Dispatches a custom event (not namespaced) on the given element.
588
+ * @param element The toast element.
589
+ * @param eventType The event type.
590
+ * @param payload Optional event detail payload.
591
+ */
592
+ private static _dispatchEventOnElement(
593
+ element: HTMLElement,
594
+ eventType: string,
595
+ payload?: object,
596
+ ) {
597
+ const event = new CustomEvent(eventType, { detail: payload });
598
+ element.dispatchEvent(event);
599
+ }
600
+
601
+ /**
602
+ * Initialize toast system (placeholder for future use).
603
+ */
604
+ public static init(): void {}
605
+ }