mtrl 0.2.2 → 0.2.4

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 (97) hide show
  1. package/.typedocignore +11 -0
  2. package/DOCS.md +153 -0
  3. package/index.ts +18 -3
  4. package/package.json +7 -2
  5. package/src/components/badge/_styles.scss +174 -0
  6. package/src/components/badge/api.ts +292 -0
  7. package/src/components/badge/badge.ts +52 -0
  8. package/src/components/badge/config.ts +68 -0
  9. package/src/components/badge/constants.ts +30 -0
  10. package/src/components/badge/features.ts +185 -0
  11. package/src/components/badge/index.ts +4 -0
  12. package/src/components/badge/types.ts +105 -0
  13. package/src/components/button/types.ts +174 -29
  14. package/src/components/carousel/_styles.scss +645 -0
  15. package/src/components/carousel/api.ts +147 -0
  16. package/src/components/carousel/carousel.ts +178 -0
  17. package/src/components/carousel/config.ts +91 -0
  18. package/src/components/carousel/constants.ts +95 -0
  19. package/src/components/carousel/features/drag.ts +388 -0
  20. package/src/components/carousel/features/index.ts +8 -0
  21. package/src/components/carousel/features/slides.ts +682 -0
  22. package/src/components/carousel/index.ts +38 -0
  23. package/src/components/carousel/types.ts +327 -0
  24. package/src/components/dialog/_styles.scss +213 -0
  25. package/src/components/dialog/api.ts +283 -0
  26. package/src/components/dialog/config.ts +113 -0
  27. package/src/components/dialog/constants.ts +32 -0
  28. package/src/components/dialog/dialog.ts +56 -0
  29. package/src/components/dialog/features.ts +713 -0
  30. package/src/components/dialog/index.ts +15 -0
  31. package/src/components/dialog/types.ts +221 -0
  32. package/src/components/progress/_styles.scss +13 -1
  33. package/src/components/progress/api.ts +2 -2
  34. package/src/components/progress/progress.ts +2 -2
  35. package/src/components/progress/types.ts +3 -0
  36. package/src/components/radios/_styles.scss +232 -0
  37. package/src/components/radios/api.ts +100 -0
  38. package/src/components/radios/config.ts +60 -0
  39. package/src/components/radios/constants.ts +28 -0
  40. package/src/components/radios/index.ts +4 -0
  41. package/src/components/radios/radio.ts +269 -0
  42. package/src/components/radios/radios.ts +42 -0
  43. package/src/components/radios/types.ts +232 -0
  44. package/src/components/sheet/_styles.scss +236 -0
  45. package/src/components/sheet/api.ts +96 -0
  46. package/src/components/sheet/config.ts +66 -0
  47. package/src/components/sheet/constants.ts +20 -0
  48. package/src/components/sheet/features/content.ts +51 -0
  49. package/src/components/sheet/features/gestures.ts +177 -0
  50. package/src/components/sheet/features/index.ts +6 -0
  51. package/src/components/sheet/features/position.ts +42 -0
  52. package/src/components/sheet/features/state.ts +116 -0
  53. package/src/components/sheet/features/title.ts +86 -0
  54. package/src/components/sheet/index.ts +4 -0
  55. package/src/components/sheet/sheet.ts +57 -0
  56. package/src/components/sheet/types.ts +266 -0
  57. package/src/components/slider/_styles.scss +518 -0
  58. package/src/components/slider/api.ts +336 -0
  59. package/src/components/slider/config.ts +145 -0
  60. package/src/components/slider/constants.ts +28 -0
  61. package/src/components/slider/features/appearance.ts +140 -0
  62. package/src/components/slider/features/disabled.ts +43 -0
  63. package/src/components/slider/features/events.ts +164 -0
  64. package/src/components/slider/features/index.ts +5 -0
  65. package/src/components/slider/features/interactions.ts +256 -0
  66. package/src/components/slider/features/keyboard.ts +114 -0
  67. package/src/components/slider/features/slider.ts +336 -0
  68. package/src/components/slider/features/structure.ts +264 -0
  69. package/src/components/slider/features/ui.ts +518 -0
  70. package/src/components/slider/index.ts +9 -0
  71. package/src/components/slider/slider.ts +58 -0
  72. package/src/components/slider/types.ts +166 -0
  73. package/src/components/tabs/_styles.scss +224 -0
  74. package/src/components/tabs/api.ts +443 -0
  75. package/src/components/tabs/config.ts +80 -0
  76. package/src/components/tabs/constants.ts +12 -0
  77. package/src/components/tabs/index.ts +4 -0
  78. package/src/components/tabs/tabs.ts +52 -0
  79. package/src/components/tabs/types.ts +247 -0
  80. package/src/components/textfield/_styles.scss +97 -4
  81. package/src/components/tooltip/_styles.scss +241 -0
  82. package/src/components/tooltip/api.ts +411 -0
  83. package/src/components/tooltip/config.ts +78 -0
  84. package/src/components/tooltip/constants.ts +27 -0
  85. package/src/components/tooltip/index.ts +4 -0
  86. package/src/components/tooltip/tooltip.ts +60 -0
  87. package/src/components/tooltip/types.ts +178 -0
  88. package/src/core/build/_ripple.scss +79 -0
  89. package/src/core/build/constants.ts +48 -0
  90. package/src/core/build/icon.ts +137 -0
  91. package/src/core/build/ripple.ts +216 -0
  92. package/src/core/build/text.ts +91 -0
  93. package/src/index.ts +9 -1
  94. package/src/styles/abstract/_variables.scss +24 -12
  95. package/tsconfig.json +22 -0
  96. package/typedoc.json +28 -0
  97. package/typedoc.simple.json +14 -0
@@ -0,0 +1,713 @@
1
+ // src/components/dialog/features.ts
2
+ import { getOverlayConfig } from './config';
3
+ import { DIALOG_SIZES, DIALOG_ANIMATIONS, DIALOG_FOOTER_ALIGNMENTS, DIALOG_EVENTS } from './constants';
4
+ import { DialogConfig, DialogButton, DialogEvent } from './types';
5
+ import createButton from '../button';
6
+ import { BUTTON_VARIANTS } from '../button/constants';
7
+
8
+ /**
9
+ * Creates the dialog DOM structure
10
+ * @param config Dialog configuration
11
+ * @returns Component enhancer with DOM structure
12
+ */
13
+ export const withStructure = (config: DialogConfig) => component => {
14
+ // Create the overlay element
15
+ const overlayConfig = getOverlayConfig(config);
16
+ const overlay = document.createElement(overlayConfig.tag);
17
+
18
+ // Add overlay classes
19
+ overlay.classList.add(component.getClass('dialog-overlay'));
20
+
21
+ // Set overlay attributes
22
+ Object.entries(overlayConfig.attrs || {}).forEach(([key, value]) => {
23
+ if (value !== undefined) {
24
+ overlay.setAttribute(key, String(value));
25
+ }
26
+ });
27
+
28
+ // Set custom z-index if provided
29
+ if (config.zIndex) {
30
+ overlay.style.zIndex = String(config.zIndex);
31
+ }
32
+
33
+ // Create internal structure
34
+ const createHeader = () => {
35
+ const header = document.createElement('div');
36
+ header.classList.add(component.getClass('dialog-header'));
37
+
38
+ const headerContent = document.createElement('div');
39
+ headerContent.classList.add(component.getClass('dialog-header-content'));
40
+ header.appendChild(headerContent);
41
+
42
+ if (config.title) {
43
+ const title = document.createElement('h2');
44
+ title.classList.add(component.getClass('dialog-header-title'));
45
+ title.textContent = config.title;
46
+ headerContent.appendChild(title);
47
+ }
48
+
49
+ if (config.subtitle) {
50
+ const subtitle = document.createElement('p');
51
+ subtitle.classList.add(component.getClass('dialog-header-subtitle'));
52
+ subtitle.textContent = config.subtitle;
53
+ headerContent.appendChild(subtitle);
54
+ }
55
+
56
+ if (config.closeButton !== false) {
57
+ const closeButton = document.createElement('button');
58
+ closeButton.classList.add(component.getClass('dialog-header-close'));
59
+ closeButton.setAttribute('aria-label', 'Close dialog');
60
+ closeButton.innerHTML = `
61
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
62
+ <line x1="18" y1="6" x2="6" y2="18"></line>
63
+ <line x1="6" y1="6" x2="18" y2="18"></line>
64
+ </svg>
65
+ `;
66
+ closeButton.addEventListener('click', () => {
67
+ component.events.trigger(DIALOG_EVENTS.CLOSE, { dialog: component });
68
+ });
69
+ header.appendChild(closeButton);
70
+ }
71
+
72
+ return header;
73
+ };
74
+
75
+ const createContent = () => {
76
+ const content = document.createElement('div');
77
+ content.classList.add(component.getClass('dialog-content'));
78
+
79
+ if (config.content) {
80
+ content.innerHTML = config.content;
81
+ }
82
+
83
+ return content;
84
+ };
85
+
86
+ const createFooter = () => {
87
+ const footer = document.createElement('div');
88
+ footer.classList.add(component.getClass('dialog-footer'));
89
+
90
+ // Apply footer alignment
91
+ const alignment = config.footerAlignment || DIALOG_FOOTER_ALIGNMENTS.RIGHT;
92
+ if (alignment !== DIALOG_FOOTER_ALIGNMENTS.RIGHT) {
93
+ footer.classList.add(`${component.getClass('dialog-footer')}--${alignment}`);
94
+ }
95
+
96
+ // Add buttons if provided
97
+ if (Array.isArray(config.buttons) && config.buttons.length > 0) {
98
+ config.buttons.forEach(buttonConfig => addButton(footer, buttonConfig, component));
99
+ }
100
+
101
+ return footer;
102
+ };
103
+
104
+ // Create the dialog structure
105
+ const header = createHeader();
106
+ const content = createContent();
107
+ const footer = Array.isArray(config.buttons) && config.buttons.length > 0 ? createFooter() : null;
108
+
109
+ // Add header, content, and footer to dialog
110
+ component.element.appendChild(header);
111
+ component.element.appendChild(content);
112
+ if (footer) {
113
+ component.element.appendChild(footer);
114
+ }
115
+
116
+ // Add dialog to overlay
117
+ overlay.appendChild(component.element);
118
+
119
+ // Add dialog classes
120
+ component.element.classList.add(component.getClass('dialog'));
121
+
122
+ // Apply size class
123
+ const size = config.size || DIALOG_SIZES.MEDIUM;
124
+ if (size !== DIALOG_SIZES.MEDIUM) {
125
+ component.element.classList.add(`${component.getClass('dialog')}--${size}`);
126
+ }
127
+
128
+ // Apply animation class
129
+ const animation = config.animation || DIALOG_ANIMATIONS.SCALE;
130
+ if (animation !== DIALOG_ANIMATIONS.SCALE) {
131
+ component.element.classList.add(`${component.getClass('dialog')}--${animation}`);
132
+ }
133
+
134
+ // Add overlay to container or document.body
135
+ const container = config.container || document.body;
136
+ container.appendChild(overlay);
137
+
138
+ // Store elements in component
139
+ return {
140
+ ...component,
141
+ overlay,
142
+ structure: {
143
+ header,
144
+ content,
145
+ footer,
146
+ container
147
+ }
148
+ };
149
+ };
150
+
151
+ /**
152
+ * Adds button to dialog footer
153
+ * @param footer Footer element
154
+ * @param buttonConfig Button configuration
155
+ * @param component Dialog component
156
+ */
157
+ const addButton = (footer: HTMLElement, buttonConfig: DialogButton, component: any) => {
158
+ const {
159
+ text,
160
+ variant = BUTTON_VARIANTS.TEXT,
161
+ onClick,
162
+ closeDialog = true,
163
+ autofocus = false,
164
+ attrs = {}
165
+ } = buttonConfig;
166
+
167
+ const button = createButton({
168
+ text,
169
+ variant,
170
+ ...attrs
171
+ });
172
+
173
+ // Add click handler
174
+ button.on('click', event => {
175
+ let shouldClose = closeDialog;
176
+
177
+ // Call onClick handler if provided
178
+ if (typeof onClick === 'function') {
179
+ const result = onClick(event, component);
180
+ // If onClick returns false, don't close the dialog
181
+ if (result === false) {
182
+ shouldClose = false;
183
+ }
184
+ }
185
+
186
+ // Close dialog if needed
187
+ if (shouldClose) {
188
+ component.events.trigger(DIALOG_EVENTS.CLOSE, { dialog: component });
189
+ }
190
+ });
191
+
192
+ // Set autofocus attribute if needed
193
+ if (autofocus) {
194
+ button.element.setAttribute('autofocus', 'true');
195
+ }
196
+
197
+ footer.appendChild(button.element);
198
+
199
+ // Store button instance
200
+ if (!component._buttons) {
201
+ component._buttons = [];
202
+ }
203
+
204
+ component._buttons.push({
205
+ config: buttonConfig,
206
+ instance: button
207
+ });
208
+ };
209
+
210
+ /**
211
+ * Add visibility control to dialog
212
+ * @returns Component enhancer with visibility features
213
+ */
214
+ export const withVisibility = () => component => {
215
+ // Initial state
216
+ const isOpen = component.config.open === true;
217
+
218
+ // Setup animation duration
219
+ const animationDuration = component.config.animationDuration || 150;
220
+
221
+ // Helper functions to handle focus trap
222
+ const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
223
+ let previouslyFocusedElement: HTMLElement | null = null;
224
+
225
+ const trapFocus = () => {
226
+ if (!component.config.trapFocus) return;
227
+
228
+ const focusableContent = component.element.querySelectorAll(focusableElements);
229
+ if (focusableContent.length === 0) return;
230
+
231
+ const firstFocusableElement = focusableContent[0] as HTMLElement;
232
+ const lastFocusableElement = focusableContent[focusableContent.length - 1] as HTMLElement;
233
+
234
+ // Focus the first element if autofocus is enabled
235
+ if (component.config.autofocus) {
236
+ // Check for a button with autofocus attribute
237
+ const autofocusElement = component.element.querySelector('[autofocus]') as HTMLElement;
238
+ if (autofocusElement) {
239
+ autofocusElement.focus();
240
+ } else {
241
+ firstFocusableElement.focus();
242
+ }
243
+ }
244
+
245
+ // Set up the keyboard trap
246
+ component.element.addEventListener('keydown', handleKeyDown);
247
+
248
+ function handleKeyDown(e: KeyboardEvent) {
249
+ const isTabPressed = e.key === 'Tab';
250
+
251
+ if (!isTabPressed) return;
252
+
253
+ if (e.shiftKey) {
254
+ // If shift + tab pressed and focus is on first element, move to last
255
+ if (document.activeElement === firstFocusableElement) {
256
+ lastFocusableElement.focus();
257
+ e.preventDefault();
258
+ }
259
+ } else {
260
+ // If tab pressed and focus is on last element, move to first
261
+ if (document.activeElement === lastFocusableElement) {
262
+ firstFocusableElement.focus();
263
+ e.preventDefault();
264
+ }
265
+ }
266
+ }
267
+ };
268
+
269
+ const releaseFocus = () => {
270
+ if (!component.config.trapFocus) return;
271
+
272
+ component.element.removeEventListener('keydown', handleKeyDown);
273
+
274
+ // Restore focus to previously focused element
275
+ if (previouslyFocusedElement) {
276
+ previouslyFocusedElement.focus();
277
+ previouslyFocusedElement = null;
278
+ }
279
+
280
+ function handleKeyDown() {}
281
+ };
282
+
283
+ const setupEvents = () => {
284
+ // Handle overlay click
285
+ if (component.config.closeOnOverlayClick !== false) {
286
+ component.overlay.addEventListener('click', handleOverlayClick);
287
+ }
288
+
289
+ // Handle Escape key
290
+ if (component.config.closeOnEscape !== false) {
291
+ document.addEventListener('keydown', handleEscKey);
292
+ }
293
+ };
294
+
295
+ const cleanupEvents = () => {
296
+ component.overlay.removeEventListener('click', handleOverlayClick);
297
+ document.removeEventListener('keydown', handleEscKey);
298
+ };
299
+
300
+ function handleOverlayClick(e: MouseEvent) {
301
+ // Only close if the click was directly on the overlay
302
+ if (e.target === component.overlay) {
303
+ component.events.trigger(DIALOG_EVENTS.CLOSE, {
304
+ dialog: component,
305
+ originalEvent: e
306
+ });
307
+ }
308
+ }
309
+
310
+ function handleEscKey(e: KeyboardEvent) {
311
+ if (e.key === 'Escape' && component.visibility.isOpen()) {
312
+ component.events.trigger(DIALOG_EVENTS.CLOSE, {
313
+ dialog: component,
314
+ originalEvent: e
315
+ });
316
+ }
317
+ }
318
+
319
+ // Setup initial state
320
+ if (isOpen) {
321
+ component.overlay.classList.add(`${component.getClass('dialog-overlay')}--visible`);
322
+ component.element.classList.add(`${component.getClass('dialog')}--visible`);
323
+
324
+ // Setup focus trap and events
325
+ trapFocus();
326
+ setupEvents();
327
+ }
328
+
329
+ return {
330
+ ...component,
331
+ visibility: {
332
+ open() {
333
+ // Don't do anything if already open
334
+ if (this.isOpen()) return;
335
+
336
+ // Store the currently focused element
337
+ previouslyFocusedElement = document.activeElement as HTMLElement;
338
+
339
+ // Trigger before open event
340
+ const beforeOpenEvent = { dialog: component, defaultPrevented: false, preventDefault: () => { beforeOpenEvent.defaultPrevented = true; } };
341
+ component.events.trigger(DIALOG_EVENTS.BEFORE_OPEN, beforeOpenEvent);
342
+
343
+ // If event was prevented, don't open
344
+ if (beforeOpenEvent.defaultPrevented) return;
345
+
346
+ // Show the overlay
347
+ component.overlay.classList.add(`${component.getClass('dialog-overlay')}--visible`);
348
+
349
+ // Show the dialog after a small delay to allow the overlay animation to start
350
+ setTimeout(() => {
351
+ component.element.classList.add(`${component.getClass('dialog')}--visible`);
352
+
353
+ // Setup focus trap and events
354
+ trapFocus();
355
+ setupEvents();
356
+
357
+ // Trigger after open event after animation completes
358
+ setTimeout(() => {
359
+ component.events.trigger(DIALOG_EVENTS.AFTER_OPEN, { dialog: component });
360
+ }, animationDuration);
361
+
362
+ // Trigger open event
363
+ component.events.trigger(DIALOG_EVENTS.OPEN, { dialog: component });
364
+ }, 10);
365
+ },
366
+
367
+ close() {
368
+ // Don't do anything if already closed
369
+ if (!this.isOpen()) return;
370
+
371
+ // Trigger before close event
372
+ const beforeCloseEvent = { dialog: component, defaultPrevented: false, preventDefault: () => { beforeCloseEvent.defaultPrevented = true; } };
373
+ component.events.trigger(DIALOG_EVENTS.BEFORE_CLOSE, beforeCloseEvent);
374
+
375
+ // If event was prevented, don't close
376
+ if (beforeCloseEvent.defaultPrevented) return;
377
+
378
+ // Hide the dialog
379
+ component.element.classList.remove(`${component.getClass('dialog')}--visible`);
380
+
381
+ // Release focus trap and cleanup events
382
+ releaseFocus();
383
+ cleanupEvents();
384
+
385
+ // Trigger close event
386
+ component.events.trigger(DIALOG_EVENTS.CLOSE, { dialog: component });
387
+
388
+ // Hide the overlay after animation completes
389
+ setTimeout(() => {
390
+ component.overlay.classList.remove(`${component.getClass('dialog-overlay')}--visible`);
391
+
392
+ // Trigger after close event
393
+ component.events.trigger(DIALOG_EVENTS.AFTER_CLOSE, { dialog: component });
394
+ }, animationDuration);
395
+ },
396
+
397
+ toggle(open?: boolean) {
398
+ if (open === undefined) {
399
+ // Toggle based on current state
400
+ if (this.isOpen()) {
401
+ this.close();
402
+ } else {
403
+ this.open();
404
+ }
405
+ } else if (open) {
406
+ this.open();
407
+ } else {
408
+ this.close();
409
+ }
410
+ },
411
+
412
+ isOpen() {
413
+ return component.element.classList.contains(`${component.getClass('dialog')}--visible`);
414
+ }
415
+ },
416
+
417
+ focus: {
418
+ trapFocus,
419
+ releaseFocus
420
+ }
421
+ };
422
+ };
423
+
424
+ /**
425
+ * Adds content management features to dialog
426
+ * @returns Component enhancer with content features
427
+ */
428
+ export const withContent = () => component => {
429
+ const headerElement = component.structure.header;
430
+ const contentElement = component.structure.content;
431
+ const footerElement = component.structure.footer;
432
+
433
+ return {
434
+ ...component,
435
+ content: {
436
+ /**
437
+ * Sets dialog title
438
+ * @param title Title text
439
+ */
440
+ setTitle(title: string) {
441
+ let titleElement = headerElement.querySelector(`.${component.getClass('dialog-header-title')}`);
442
+
443
+ if (!titleElement && title) {
444
+ // Create title element if it doesn't exist
445
+ titleElement = document.createElement('h2');
446
+ titleElement.classList.add(component.getClass('dialog-header-title'));
447
+ headerElement.querySelector(`.${component.getClass('dialog-header-content')}`).appendChild(titleElement);
448
+ }
449
+
450
+ if (titleElement) {
451
+ titleElement.textContent = title;
452
+ }
453
+ },
454
+
455
+ /**
456
+ * Gets dialog title
457
+ * @returns Title text
458
+ */
459
+ getTitle() {
460
+ const titleElement = headerElement.querySelector(`.${component.getClass('dialog-header-title')}`);
461
+ return titleElement ? titleElement.textContent || '' : '';
462
+ },
463
+
464
+ /**
465
+ * Sets dialog subtitle
466
+ * @param subtitle Subtitle text
467
+ */
468
+ setSubtitle(subtitle: string) {
469
+ let subtitleElement = headerElement.querySelector(`.${component.getClass('dialog-header-subtitle')}`);
470
+
471
+ if (!subtitleElement && subtitle) {
472
+ // Create subtitle element if it doesn't exist
473
+ subtitleElement = document.createElement('p');
474
+ subtitleElement.classList.add(component.getClass('dialog-header-subtitle'));
475
+ headerElement.querySelector(`.${component.getClass('dialog-header-content')}`).appendChild(subtitleElement);
476
+ }
477
+
478
+ if (subtitleElement) {
479
+ subtitleElement.textContent = subtitle;
480
+ }
481
+ },
482
+
483
+ /**
484
+ * Gets dialog subtitle
485
+ * @returns Subtitle text
486
+ */
487
+ getSubtitle() {
488
+ const subtitleElement = headerElement.querySelector(`.${component.getClass('dialog-header-subtitle')}`);
489
+ return subtitleElement ? subtitleElement.textContent || '' : '';
490
+ },
491
+
492
+ /**
493
+ * Sets dialog content
494
+ * @param content Content HTML
495
+ */
496
+ setContent(content: string) {
497
+ contentElement.innerHTML = content;
498
+ },
499
+
500
+ /**
501
+ * Gets dialog content
502
+ * @returns Content HTML
503
+ */
504
+ getContent() {
505
+ return contentElement.innerHTML;
506
+ },
507
+
508
+ /**
509
+ * Gets dialog header element
510
+ * @returns Header element
511
+ */
512
+ getHeaderElement() {
513
+ return headerElement;
514
+ },
515
+
516
+ /**
517
+ * Gets dialog content element
518
+ * @returns Content element
519
+ */
520
+ getContentElement() {
521
+ return contentElement;
522
+ },
523
+
524
+ /**
525
+ * Gets dialog footer element
526
+ * @returns Footer element
527
+ */
528
+ getFooterElement() {
529
+ return footerElement;
530
+ }
531
+ }
532
+ };
533
+ };
534
+
535
+ /**
536
+ * Adds button management features to dialog
537
+ * @returns Component enhancer with button features
538
+ */
539
+ export const withButtons = () => component => {
540
+ // Initialize buttons array if not already done
541
+ if (!component._buttons) {
542
+ component._buttons = [];
543
+ }
544
+
545
+ return {
546
+ ...component,
547
+ buttons: {
548
+ /**
549
+ * Adds a button to the dialog footer
550
+ * @param button Button configuration
551
+ */
552
+ addButton(button: DialogButton) {
553
+ // Create footer if it doesn't exist
554
+ let footer = component.structure.footer;
555
+
556
+ if (!footer) {
557
+ footer = document.createElement('div');
558
+ footer.classList.add(component.getClass('dialog-footer'));
559
+
560
+ // Apply footer alignment
561
+ const alignment = component.config.footerAlignment || DIALOG_FOOTER_ALIGNMENTS.RIGHT;
562
+ if (alignment !== DIALOG_FOOTER_ALIGNMENTS.RIGHT) {
563
+ footer.classList.add(`${component.getClass('dialog-footer')}--${alignment}`);
564
+ }
565
+
566
+ component.element.appendChild(footer);
567
+ component.structure.footer = footer;
568
+ }
569
+
570
+ // Add the button
571
+ addButton(footer, button, component);
572
+ },
573
+
574
+ /**
575
+ * Removes a button by index or text
576
+ * @param indexOrText Button index or text
577
+ */
578
+ removeButton(indexOrText: number | string) {
579
+ if (typeof indexOrText === 'number') {
580
+ // Remove by index
581
+ if (indexOrText >= 0 && indexOrText < component._buttons.length) {
582
+ const button = component._buttons[indexOrText];
583
+ button.instance.destroy();
584
+ component._buttons.splice(indexOrText, 1);
585
+ }
586
+ } else {
587
+ // Remove by text
588
+ const index = component._buttons.findIndex(button =>
589
+ button.config.text === indexOrText);
590
+
591
+ if (index !== -1) {
592
+ const button = component._buttons[index];
593
+ button.instance.destroy();
594
+ component._buttons.splice(index, 1);
595
+ }
596
+ }
597
+
598
+ // If no buttons left, remove footer
599
+ if (component._buttons.length === 0 && component.structure.footer) {
600
+ component.element.removeChild(component.structure.footer);
601
+ component.structure.footer = null;
602
+ }
603
+ },
604
+
605
+ /**
606
+ * Gets all footer buttons
607
+ * @returns Array of button configurations
608
+ */
609
+ getButtons() {
610
+ return component._buttons.map(button => button.config);
611
+ },
612
+
613
+ /**
614
+ * Sets footer alignment
615
+ * @param alignment Footer alignment
616
+ */
617
+ setFooterAlignment(alignment: keyof typeof DIALOG_FOOTER_ALIGNMENTS | DIALOG_FOOTER_ALIGNMENTS) {
618
+ if (!component.structure.footer) return;
619
+
620
+ // Remove existing alignment classes
621
+ Object.values(DIALOG_FOOTER_ALIGNMENTS).forEach(align => {
622
+ if (align !== DIALOG_FOOTER_ALIGNMENTS.RIGHT) {
623
+ component.structure.footer.classList.remove(`${component.getClass('dialog-footer')}--${align}`);
624
+ }
625
+ });
626
+
627
+ // Add new alignment class if not right (default)
628
+ if (alignment !== DIALOG_FOOTER_ALIGNMENTS.RIGHT) {
629
+ component.structure.footer.classList.add(`${component.getClass('dialog-footer')}--${alignment}`);
630
+ }
631
+ }
632
+ }
633
+ };
634
+ };
635
+
636
+ /**
637
+ * Adds size management features to dialog
638
+ * @returns Component enhancer with size features
639
+ */
640
+ export const withSize = () => component => {
641
+ return {
642
+ ...component,
643
+ size: {
644
+ /**
645
+ * Sets dialog size
646
+ * @param size Size variant
647
+ */
648
+ setSize(size: keyof typeof DIALOG_SIZES | DIALOG_SIZES) {
649
+ // Remove existing size classes
650
+ Object.values(DIALOG_SIZES).forEach(sizeValue => {
651
+ component.element.classList.remove(`${component.getClass('dialog')}--${sizeValue}`);
652
+ });
653
+
654
+ // Add new size class if not medium (default)
655
+ if (size !== DIALOG_SIZES.MEDIUM) {
656
+ component.element.classList.add(`${component.getClass('dialog')}--${size}`);
657
+ }
658
+ }
659
+ }
660
+ };
661
+ };
662
+
663
+ /**
664
+ * Adds confirmation dialog features
665
+ * @returns Component enhancer with confirm feature
666
+ */
667
+ export const withConfirm = () => component => {
668
+ return {
669
+ ...component,
670
+ confirm(options) {
671
+ return new Promise((resolve) => {
672
+ const {
673
+ title = 'Confirm',
674
+ message,
675
+ confirmText = 'Yes',
676
+ cancelText = 'No',
677
+ confirmVariant = BUTTON_VARIANTS.FILLED,
678
+ cancelVariant = BUTTON_VARIANTS.TEXT,
679
+ size = DIALOG_SIZES.SMALL
680
+ } = options;
681
+
682
+ // Set dialog properties
683
+ component.content.setTitle(title);
684
+ component.content.setContent(`<p>${message}</p>`);
685
+ component.size.setSize(size);
686
+
687
+ // Clear existing buttons
688
+ component._buttons.forEach(button => button.instance.destroy());
689
+ component._buttons = [];
690
+
691
+ // Add confirm and cancel buttons
692
+ component.buttons.addButton({
693
+ text: confirmText,
694
+ variant: confirmVariant,
695
+ onClick: () => {
696
+ resolve(true);
697
+ }
698
+ });
699
+
700
+ component.buttons.addButton({
701
+ text: cancelText,
702
+ variant: cancelVariant,
703
+ onClick: () => {
704
+ resolve(false);
705
+ }
706
+ });
707
+
708
+ // Open the dialog
709
+ component.visibility.open();
710
+ });
711
+ }
712
+ };
713
+ };