@makolabs/ripple 1.7.11 → 1.9.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 (135) hide show
  1. package/README.md +16 -0
  2. package/dist/adapters/ai/OpenAIAdapter.d.ts +8 -1
  3. package/dist/adapters/ai/OpenAIAdapter.js +2 -2
  4. package/dist/adapters/storage/BaseAdapter.js +2 -2
  5. package/dist/adapters/storage/S3Adapter.js +1 -6
  6. package/dist/adapters/storage/types.d.ts +3 -3
  7. package/dist/ai/AIChatInterface.svelte +0 -1
  8. package/dist/ai/ai-chat-interface.d.ts +21 -22
  9. package/dist/ai/ai-types.d.ts +44 -0
  10. package/dist/ai/ai-types.js +1 -0
  11. package/dist/ai/content-detector.js +0 -1
  12. package/dist/button/Button.svelte +9 -2
  13. package/dist/button/button-types.d.ts +25 -0
  14. package/dist/button/button-types.js +1 -0
  15. package/dist/button/button.d.ts +39 -40
  16. package/dist/charts/Chart.svelte +11 -23
  17. package/dist/charts/chart-types.d.ts +137 -0
  18. package/dist/charts/chart-types.js +1 -0
  19. package/dist/drawer/Drawer.svelte +57 -23
  20. package/dist/drawer/drawer-types.d.ts +33 -0
  21. package/dist/drawer/drawer-types.js +1 -0
  22. package/dist/drawer/drawer.d.ts +18 -19
  23. package/dist/elements/accordion/Accordion.svelte +39 -18
  24. package/dist/elements/accordion/accordion-types.d.ts +29 -0
  25. package/dist/elements/accordion/accordion-types.js +1 -0
  26. package/dist/elements/accordion/accordion.d.ts +21 -22
  27. package/dist/elements/alert/Alert.svelte +20 -8
  28. package/dist/elements/badge/Badge.svelte +5 -2
  29. package/dist/elements/badge/badge-types.d.ts +11 -0
  30. package/dist/elements/badge/badge-types.js +1 -0
  31. package/dist/elements/badge/badge.d.ts +39 -40
  32. package/dist/elements/dropdown/Dropdown.svelte +18 -2
  33. package/dist/elements/dropdown/Select.svelte +17 -5
  34. package/dist/elements/dropdown/dropdown-types.d.ts +68 -0
  35. package/dist/elements/dropdown/dropdown-types.js +1 -0
  36. package/dist/elements/dropdown/dropdown.d.ts +18 -19
  37. package/dist/elements/dropdown/select.d.ts +18 -19
  38. package/dist/elements/file-upload/file-upload-types.d.ts +68 -0
  39. package/dist/elements/file-upload/file-upload-types.js +1 -0
  40. package/dist/elements/pagination/Pagination.svelte +15 -2
  41. package/dist/elements/pagination/Pagination.svelte.d.ts +1 -0
  42. package/dist/elements/progress/progress-types.d.ts +22 -0
  43. package/dist/elements/progress/progress-types.js +1 -0
  44. package/dist/elements/timeline/timeline-types.d.ts +11 -0
  45. package/dist/elements/timeline/timeline-types.js +1 -0
  46. package/dist/filters/filter-types.d.ts +24 -0
  47. package/dist/filters/filter-types.js +1 -0
  48. package/dist/forms/Checkbox.svelte +16 -4
  49. package/dist/forms/Form.svelte +0 -2
  50. package/dist/forms/Input.svelte +19 -5
  51. package/dist/forms/NumberInput.svelte +8 -1
  52. package/dist/forms/RadioInputs.svelte +14 -5
  53. package/dist/forms/Slider.svelte +6 -4
  54. package/dist/forms/Toggle.svelte +67 -29
  55. package/dist/forms/form-types.d.ts +168 -0
  56. package/dist/forms/form-types.js +1 -0
  57. package/dist/forms/slider.d.ts +72 -10
  58. package/dist/forms/slider.js +21 -0
  59. package/dist/header/Breadcrumbs.svelte +47 -24
  60. package/dist/header/PageHeader.svelte +12 -2
  61. package/dist/header/breadcrumbs.d.ts +47 -39
  62. package/dist/header/header-types.d.ts +43 -0
  63. package/dist/header/header-types.js +1 -0
  64. package/dist/helper/deprecation.d.ts +14 -0
  65. package/dist/helper/deprecation.js +24 -0
  66. package/dist/helper/testid.d.ts +10 -0
  67. package/dist/helper/testid.js +17 -0
  68. package/dist/index.d.ts +37 -1007
  69. package/dist/index.js +38 -14
  70. package/dist/layout/activity-list/activity-list-types.d.ts +30 -0
  71. package/dist/layout/activity-list/activity-list-types.js +1 -0
  72. package/dist/layout/activity-list/activity-list.d.ts +21 -22
  73. package/dist/layout/card/Card.svelte +19 -5
  74. package/dist/layout/card/card-types.d.ts +43 -0
  75. package/dist/layout/card/card-types.js +1 -0
  76. package/dist/layout/card/card.d.ts +21 -22
  77. package/dist/layout/card/metric-card.d.ts +3 -3
  78. package/dist/layout/card/ranked-card.d.ts +2 -1
  79. package/dist/layout/navbar/Navbar.svelte +14 -16
  80. package/dist/layout/navbar/navbar-types.d.ts +19 -0
  81. package/dist/layout/navbar/navbar-types.js +1 -0
  82. package/dist/layout/navbar/navbar.d.ts +19 -19
  83. package/dist/layout/sidebar/Sidebar.svelte +6 -3
  84. package/dist/layout/sidebar/sidebar-types.d.ts +59 -0
  85. package/dist/layout/sidebar/sidebar-types.js +1 -0
  86. package/dist/layout/table/Table.svelte +237 -303
  87. package/dist/layout/table/table-types.d.ts +82 -0
  88. package/dist/layout/table/table-types.js +1 -0
  89. package/dist/layout/table/table.d.ts +24 -25
  90. package/dist/layout/tabs/Tab.svelte +3 -1
  91. package/dist/layout/tabs/TabGroup.svelte +7 -4
  92. package/dist/layout/tabs/tabs-types.d.ts +43 -0
  93. package/dist/layout/tabs/tabs-types.js +1 -0
  94. package/dist/layout/tabs/tabs.d.ts +39 -40
  95. package/dist/modal/Modal.svelte +124 -21
  96. package/dist/modal/modal-types.d.ts +34 -0
  97. package/dist/modal/modal-types.js +1 -0
  98. package/dist/modal/modal.d.ts +18 -19
  99. package/dist/modal/modal.js +2 -2
  100. package/dist/types/echarts.d.ts +27 -0
  101. package/dist/user-management/UserModal.svelte +1 -1
  102. package/dist/user-management/UserTable.svelte +3 -3
  103. package/dist/user-management/UserViewModal.svelte +2 -2
  104. package/dist/user-management/user-management-types.d.ts +156 -0
  105. package/dist/user-management/user-management-types.js +1 -0
  106. package/dist/variants.d.ts +13 -13
  107. package/package.json +9 -7
  108. package/dist/ai/AIChatInterfaceTestWrapper.svelte +0 -26
  109. package/dist/ai/AIChatInterfaceTestWrapper.svelte.d.ts +0 -17
  110. package/dist/button/ButtonTestWrapper.svelte +0 -10
  111. package/dist/button/ButtonTestWrapper.svelte.d.ts +0 -7
  112. package/dist/drawer/DrawerTestWrapper.svelte +0 -19
  113. package/dist/drawer/DrawerTestWrapper.svelte.d.ts +0 -9
  114. package/dist/elements/accordion/AccordionTestWrapper.svelte +0 -21
  115. package/dist/elements/accordion/AccordionTestWrapper.svelte.d.ts +0 -10
  116. package/dist/elements/badge/BadgeTestWrapper.svelte +0 -14
  117. package/dist/elements/badge/BadgeTestWrapper.svelte.d.ts +0 -9
  118. package/dist/forms/CheckboxTestWrapper.svelte +0 -8
  119. package/dist/forms/CheckboxTestWrapper.svelte.d.ts +0 -4
  120. package/dist/forms/InputTestWrapper.svelte +0 -8
  121. package/dist/forms/InputTestWrapper.svelte.d.ts +0 -4
  122. package/dist/forms/ToggleTestWrapper.svelte +0 -8
  123. package/dist/forms/ToggleTestWrapper.svelte.d.ts +0 -7
  124. package/dist/layout/card/CardTestWrapper.svelte +0 -15
  125. package/dist/layout/card/CardTestWrapper.svelte.d.ts +0 -7
  126. package/dist/modal/ModalTestWrapper.svelte +0 -20
  127. package/dist/modal/ModalTestWrapper.svelte.d.ts +0 -8
  128. package/dist/user-management/UserManagementTestWrapper.svelte +0 -32
  129. package/dist/user-management/UserManagementTestWrapper.svelte.d.ts +0 -12
  130. package/dist/user-management/UserModalTestWrapper.svelte +0 -22
  131. package/dist/user-management/UserModalTestWrapper.svelte.d.ts +0 -7
  132. package/dist/user-management/UserTableTestWrapper.svelte +0 -41
  133. package/dist/user-management/UserTableTestWrapper.svelte.d.ts +0 -7
  134. package/dist/user-management/UserViewModalTestWrapper.svelte +0 -22
  135. package/dist/user-management/UserViewModalTestWrapper.svelte.d.ts +0 -7
@@ -0,0 +1,82 @@
1
+ import type { ClassValue } from 'tailwind-variants';
2
+ import type { Snippet } from 'svelte';
3
+ export type DataRow = Record<string, any>;
4
+ export type KeyType = keyof DataRow;
5
+ export type StatusType = 'active' | 'inactive' | 'pending' | 'error' | 'default';
6
+ export type TableColumn<T extends DataRow = any> = {
7
+ key: KeyType;
8
+ header: string;
9
+ cell?: Snippet<[row: T, key: KeyType, index?: number]>;
10
+ sortable?: boolean;
11
+ sortKey?: string;
12
+ align?: 'left' | 'center' | 'right';
13
+ width?: string;
14
+ class?: ClassValue;
15
+ };
16
+ export type SortDirection = 'asc' | 'desc' | null;
17
+ export type SortState = {
18
+ column: string | null;
19
+ direction: SortDirection;
20
+ };
21
+ export type TableProps<T extends DataRow = any> = {
22
+ data: T[];
23
+ columns: TableColumn<T>[];
24
+ bordered?: boolean;
25
+ striped?: boolean;
26
+ pageSize?: number;
27
+ currentPage?: number;
28
+ totalItems?: number;
29
+ selectable?: boolean;
30
+ selected?: T[];
31
+ class?: ClassValue;
32
+ /** @deprecated Use wrapperClass instead */
33
+ wrapperclass?: ClassValue;
34
+ wrapperClass?: ClassValue;
35
+ /** @deprecated Use tableClass instead */
36
+ tableclass?: ClassValue;
37
+ tableClass?: ClassValue;
38
+ /** @deprecated Use theadClass instead */
39
+ theadclass?: ClassValue;
40
+ theadClass?: ClassValue;
41
+ /** @deprecated Use tbodyClass instead */
42
+ tbodyclass?: ClassValue;
43
+ tbodyClass?: ClassValue;
44
+ /** @deprecated Use trClass instead */
45
+ trclass?: ClassValue;
46
+ trClass?: ClassValue;
47
+ /** @deprecated Use thClass instead */
48
+ thclass?: ClassValue;
49
+ thClass?: ClassValue;
50
+ /** @deprecated Use tdClass instead */
51
+ tdclass?: ClassValue;
52
+ tdClass?: ClassValue;
53
+ /** @deprecated Use footerClass instead */
54
+ footerclass?: ClassValue;
55
+ footerClass?: ClassValue;
56
+ /** @deprecated Use paginationClass instead */
57
+ paginationclass?: ClassValue;
58
+ paginationClass?: ClassValue;
59
+ onrowclick?: (row: T, index: number) => void;
60
+ onsort?: (sortState: SortState) => void;
61
+ onselect?: (selected: T[]) => void;
62
+ onpagechange?: (page: number) => void;
63
+ /** @deprecated Use rowClass instead */
64
+ rowclass?: (row: T, index: number) => ClassValue;
65
+ rowClass?: (row: T, index: number) => ClassValue;
66
+ loading?: boolean;
67
+ expandedContent?: Snippet<[T]>;
68
+ pagination?: boolean;
69
+ showPagination?: boolean;
70
+ showPageSize?: boolean;
71
+ pageSizeOptions?: number[];
72
+ onpagesizechange?: (pageSize: number) => void;
73
+ paginationPosition?: 'top' | 'bottom' | 'both';
74
+ paginationTemplate?: 'simple' | 'full';
75
+ title?: string;
76
+ subtitle?: string;
77
+ headerActions?: Snippet;
78
+ selectAllScope?: 'page' | 'all';
79
+ rowKey?: string;
80
+ expandable?: boolean;
81
+ testId?: string;
82
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,3 @@
1
- import { Color } from '../../variants.js';
2
1
  export declare const table: import("tailwind-variants").TVReturnType<{
3
2
  size: {
4
3
  xs: {
@@ -27,32 +26,32 @@ export declare const table: import("tailwind-variants").TVReturnType<{
27
26
  };
28
27
  };
29
28
  color: {
30
- [Color.PRIMARY]: {
29
+ default: {
31
30
  th: string;
32
- tr: string;
33
31
  };
34
- [Color.SECONDARY]: {
32
+ primary: {
35
33
  th: string;
36
34
  tr: string;
37
35
  };
38
- [Color.INFO]: {
36
+ secondary: {
39
37
  th: string;
40
38
  tr: string;
41
39
  };
42
- [Color.SUCCESS]: {
40
+ info: {
43
41
  th: string;
44
42
  tr: string;
45
43
  };
46
- [Color.WARNING]: {
44
+ success: {
47
45
  th: string;
48
46
  tr: string;
49
47
  };
50
- [Color.DANGER]: {
48
+ warning: {
51
49
  th: string;
52
50
  tr: string;
53
51
  };
54
- default: {
52
+ danger: {
55
53
  th: string;
54
+ tr: string;
56
55
  };
57
56
  };
58
57
  bordered: {
@@ -112,32 +111,32 @@ export declare const table: import("tailwind-variants").TVReturnType<{
112
111
  };
113
112
  };
114
113
  color: {
115
- [Color.PRIMARY]: {
114
+ default: {
116
115
  th: string;
117
- tr: string;
118
116
  };
119
- [Color.SECONDARY]: {
117
+ primary: {
120
118
  th: string;
121
119
  tr: string;
122
120
  };
123
- [Color.INFO]: {
121
+ secondary: {
124
122
  th: string;
125
123
  tr: string;
126
124
  };
127
- [Color.SUCCESS]: {
125
+ info: {
128
126
  th: string;
129
127
  tr: string;
130
128
  };
131
- [Color.WARNING]: {
129
+ success: {
132
130
  th: string;
133
131
  tr: string;
134
132
  };
135
- [Color.DANGER]: {
133
+ warning: {
136
134
  th: string;
137
135
  tr: string;
138
136
  };
139
- default: {
137
+ danger: {
140
138
  th: string;
139
+ tr: string;
141
140
  };
142
141
  };
143
142
  bordered: {
@@ -197,32 +196,32 @@ export declare const table: import("tailwind-variants").TVReturnType<{
197
196
  };
198
197
  };
199
198
  color: {
200
- [Color.PRIMARY]: {
199
+ default: {
201
200
  th: string;
202
- tr: string;
203
201
  };
204
- [Color.SECONDARY]: {
202
+ primary: {
205
203
  th: string;
206
204
  tr: string;
207
205
  };
208
- [Color.INFO]: {
206
+ secondary: {
209
207
  th: string;
210
208
  tr: string;
211
209
  };
212
- [Color.SUCCESS]: {
210
+ info: {
213
211
  th: string;
214
212
  tr: string;
215
213
  };
216
- [Color.WARNING]: {
214
+ success: {
217
215
  th: string;
218
216
  tr: string;
219
217
  };
220
- [Color.DANGER]: {
218
+ warning: {
221
219
  th: string;
222
220
  tr: string;
223
221
  };
224
- default: {
222
+ danger: {
225
223
  th: string;
224
+ tr: string;
226
225
  };
227
226
  };
228
227
  bordered: {
@@ -12,7 +12,8 @@
12
12
  color = Color.PRIMARY,
13
13
  size = Size.BASE,
14
14
  onclick = () => {},
15
- variant = 'line'
15
+ variant = 'line',
16
+ testId
16
17
  }: TabProps = $props();
17
18
 
18
19
  function handleClick(event: Event & { currentTarget: EventTarget & HTMLButtonElement }) {
@@ -43,6 +44,7 @@
43
44
  id={`tab-${value}`}
44
45
  aria-controls={`panel-${value}`}
45
46
  aria-selected={selected ? 'true' : 'false'}
47
+ data-testid={testId}
46
48
  onclick={handleClick}
47
49
  onkeydown={(e) => {
48
50
  if (e.key === 'Enter' || e.key === ' ') {
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../../helper/cls.js';
3
+ import { buildTestId } from '../../helper/testid.js';
3
4
  import Tab from './Tab.svelte';
4
5
  import { tabs } from './tabs.js';
5
6
  import type { TabsGroupProps } from '../../index.js';
@@ -16,7 +17,8 @@
16
17
  listClass = '',
17
18
  panelClass = '',
18
19
  onchange = () => {},
19
- children
20
+ children,
21
+ testId
20
22
  }: TabsGroupProps = $props();
21
23
 
22
24
  const { base, list, panel } = $derived(
@@ -40,8 +42,8 @@
40
42
  </script>
41
43
 
42
44
  <div class={baseClass}>
43
- <div class={listClass_} role="tablist">
44
- {#each tabItems as tab (tab.value)}
45
+ <div class={listClass_} role="tablist" data-testid={buildTestId('tabgroup', 'list', testId)}>
46
+ {#each tabItems as tab, index (tab.value)}
45
47
  <Tab
46
48
  value={tab.value}
47
49
  label={tab.label}
@@ -51,12 +53,13 @@
51
53
  {color}
52
54
  {size}
53
55
  {variant}
56
+ testId={buildTestId('tabgroup', 'tab', testId, index)}
54
57
  onclick={() => handleTabClick(tab.value)}
55
58
  />
56
59
  {/each}
57
60
  </div>
58
61
 
59
- <div class={panelClass_}>
62
+ <div class={panelClass_} data-testid={buildTestId('tabgroup', 'panel', testId)}>
60
63
  {@render children?.(selected)}
61
64
  </div>
62
65
  </div>
@@ -0,0 +1,43 @@
1
+ import type { ClassValue } from 'tailwind-variants';
2
+ import type { Snippet } from 'svelte';
3
+ import type { Component } from 'svelte';
4
+ import type { VariantColors, VariantSizes } from '../../index.js';
5
+ export type TabItem = {
6
+ value: string;
7
+ label: string;
8
+ icon?: Component;
9
+ disabled?: boolean;
10
+ };
11
+ export type TabProps = {
12
+ value: string;
13
+ label: string;
14
+ icon?: Component;
15
+ selected?: boolean;
16
+ disabled?: boolean;
17
+ color?: VariantColors;
18
+ size?: VariantSizes;
19
+ variant?: 'line' | 'pill';
20
+ onclick?: (event: Event) => void;
21
+ testId?: string;
22
+ };
23
+ export type TabsGroupProps = {
24
+ tabs: TabItem[];
25
+ selected?: string;
26
+ color?: VariantColors;
27
+ size?: VariantSizes;
28
+ variant?: 'line' | 'pill';
29
+ class?: ClassValue;
30
+ listClass?: ClassValue;
31
+ triggerClass?: ClassValue;
32
+ panelClass?: ClassValue;
33
+ children?: Snippet<[active: string]>;
34
+ onchange?: (value: string) => void;
35
+ testId?: string;
36
+ };
37
+ export type TabContentProps = {
38
+ value: string;
39
+ persisted?: boolean;
40
+ panelClass?: ClassValue;
41
+ children?: Snippet<[value: string]>;
42
+ testId?: string;
43
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,31 +1,30 @@
1
- import { Color, Size } from '../../index.js';
2
1
  export declare const tabs: import("tailwind-variants").TVReturnType<{
3
2
  color: {
4
- [Color.PRIMARY]: {};
5
- [Color.SECONDARY]: {};
6
- [Color.SUCCESS]: {};
7
- [Color.WARNING]: {};
8
- [Color.DANGER]: {};
9
- [Color.INFO]: {};
10
- [Color.DEFAULT]: {};
3
+ primary: {};
4
+ secondary: {};
5
+ success: {};
6
+ warning: {};
7
+ danger: {};
8
+ info: {};
9
+ default: {};
11
10
  };
12
11
  size: {
13
- [Size.XS]: {
12
+ xs: {
14
13
  trigger: string;
15
14
  };
16
- [Size.SM]: {
15
+ sm: {
17
16
  trigger: string;
18
17
  };
19
- [Size.BASE]: {
18
+ base: {
20
19
  trigger: string;
21
20
  };
22
- [Size.LG]: {
21
+ lg: {
23
22
  trigger: string;
24
23
  };
25
- [Size.XL]: {
24
+ xl: {
26
25
  trigger: string;
27
26
  };
28
- [Size.XXL]: {
27
+ "2xl": {
29
28
  trigger: string;
30
29
  };
31
30
  };
@@ -47,31 +46,31 @@ export declare const tabs: import("tailwind-variants").TVReturnType<{
47
46
  panel: string;
48
47
  }, undefined, {
49
48
  color: {
50
- [Color.PRIMARY]: {};
51
- [Color.SECONDARY]: {};
52
- [Color.SUCCESS]: {};
53
- [Color.WARNING]: {};
54
- [Color.DANGER]: {};
55
- [Color.INFO]: {};
56
- [Color.DEFAULT]: {};
49
+ primary: {};
50
+ secondary: {};
51
+ success: {};
52
+ warning: {};
53
+ danger: {};
54
+ info: {};
55
+ default: {};
57
56
  };
58
57
  size: {
59
- [Size.XS]: {
58
+ xs: {
60
59
  trigger: string;
61
60
  };
62
- [Size.SM]: {
61
+ sm: {
63
62
  trigger: string;
64
63
  };
65
- [Size.BASE]: {
64
+ base: {
66
65
  trigger: string;
67
66
  };
68
- [Size.LG]: {
67
+ lg: {
69
68
  trigger: string;
70
69
  };
71
- [Size.XL]: {
70
+ xl: {
72
71
  trigger: string;
73
72
  };
74
- [Size.XXL]: {
73
+ "2xl": {
75
74
  trigger: string;
76
75
  };
77
76
  };
@@ -93,31 +92,31 @@ export declare const tabs: import("tailwind-variants").TVReturnType<{
93
92
  panel: string;
94
93
  }, import("tailwind-variants").TVReturnType<{
95
94
  color: {
96
- [Color.PRIMARY]: {};
97
- [Color.SECONDARY]: {};
98
- [Color.SUCCESS]: {};
99
- [Color.WARNING]: {};
100
- [Color.DANGER]: {};
101
- [Color.INFO]: {};
102
- [Color.DEFAULT]: {};
95
+ primary: {};
96
+ secondary: {};
97
+ success: {};
98
+ warning: {};
99
+ danger: {};
100
+ info: {};
101
+ default: {};
103
102
  };
104
103
  size: {
105
- [Size.XS]: {
104
+ xs: {
106
105
  trigger: string;
107
106
  };
108
- [Size.SM]: {
107
+ sm: {
109
108
  trigger: string;
110
109
  };
111
- [Size.BASE]: {
110
+ base: {
112
111
  trigger: string;
113
112
  };
114
- [Size.LG]: {
113
+ lg: {
115
114
  trigger: string;
116
115
  };
117
- [Size.XL]: {
116
+ xl: {
118
117
  trigger: string;
119
118
  };
120
- [Size.XXL]: {
119
+ "2xl": {
121
120
  trigger: string;
122
121
  };
123
122
  };
@@ -1,7 +1,11 @@
1
1
  <script lang="ts">
2
+ import { onDestroy, tick } from 'svelte';
3
+ import { browser } from '$app/environment';
2
4
  import { fade, scale } from 'svelte/transition';
3
5
  import { quintOut } from 'svelte/easing';
4
6
  import { cn } from '../helper/cls.js';
7
+ import { buildTestId } from '../helper/testid.js';
8
+ import { warnDeprecatedProps } from '../helper/deprecation.js';
5
9
  import { modal } from './modal.js';
6
10
  import type { ModalProps } from '../index.js';
7
11
 
@@ -13,32 +17,55 @@
13
17
  size,
14
18
  hideCloseButton = false,
15
19
  class: className = '',
16
- contentclass: contentClassName = '',
17
- bodyclass: bodyClassName = '',
18
- titleclass: titleClassName = '',
19
- headerclass: headerClassName = '',
20
- backdropclass: backdropClassName = '',
20
+ contentclass,
21
+ contentClass = contentclass ?? '',
22
+ bodyclass,
23
+ bodyClass = bodyclass ?? '',
24
+ titleclass,
25
+ titleClass = titleclass ?? '',
26
+ headerclass,
27
+ headerClass = headerclass ?? '',
28
+ backdropclass,
29
+ backdropClass = backdropclass ?? '',
30
+ footerclass,
31
+ footerClass = footerclass ?? '',
21
32
  children,
22
33
  footer,
23
- header
34
+ header,
35
+ testId
24
36
  }: ModalProps = $props();
25
37
 
38
+ warnDeprecatedProps(
39
+ 'Modal',
40
+ { contentclass, bodyclass, titleclass, headerclass, backdropclass, footerclass },
41
+ {
42
+ contentclass: 'contentClass',
43
+ bodyclass: 'bodyClass',
44
+ titleclass: 'titleClass',
45
+ headerclass: 'headerClass',
46
+ backdropclass: 'backdropClass',
47
+ footerclass: 'footerClass'
48
+ }
49
+ );
50
+
51
+ let modalElement: HTMLDivElement | undefined = $state();
52
+
26
53
  const styles = $derived(modal({ size }));
27
54
 
28
55
  const baseClass = $derived(cn(styles.base(), className));
29
- const backdropClass = $derived(cn(styles.backdrop(), backdropClassName));
30
- const containerClass = $derived(cn(styles.container(), contentClassName));
31
- const headerClass = $derived(cn(styles.header(), headerClassName));
32
- const titleClass = $derived(cn(styles.title(), titleClassName));
33
- const bodyClass = $derived(
56
+ const backdropClasses = $derived(cn(styles.backdrop(), backdropClass));
57
+ const containerClass = $derived(cn(styles.container(), contentClass));
58
+ const headerClasses = $derived(cn(styles.header(), headerClass));
59
+ const titleClasses = $derived(cn(styles.title(), titleClass));
60
+ const bodyClasses = $derived(
34
61
  cn(
35
62
  'flex-1 px-6 overflow-y-auto',
36
63
  // Adjust top padding based on header presence
37
64
  title || description ? 'py-4' : 'pt-6 pb-4',
38
- bodyClassName
65
+ bodyClass
39
66
  )
40
67
  );
41
- const footerClass = $derived(cn(styles.footer(), ''));
68
+ const footerClasses = $derived(cn(styles.footer(), footerClass));
42
69
  const closeClass = $derived(cn(styles.close(), ''));
43
70
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
44
71
  const descriptionClass = $derived(cn(styles.description(), ''));
@@ -53,13 +80,59 @@
53
80
  }
54
81
  }
55
82
 
83
+ let previouslyFocusedElement: HTMLElement | null = null;
84
+
85
+ function trapFocus(element: HTMLElement) {
86
+ const focusableElements = element.querySelectorAll(
87
+ 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
88
+ );
89
+
90
+ if (focusableElements.length === 0) return;
91
+
92
+ const firstElement = focusableElements[0] as HTMLElement;
93
+ const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
94
+
95
+ function handleTabKey(e: KeyboardEvent) {
96
+ if (e.key !== 'Tab') return;
97
+
98
+ if (e.shiftKey) {
99
+ if (document.activeElement === firstElement) {
100
+ lastElement.focus();
101
+ e.preventDefault();
102
+ }
103
+ } else {
104
+ if (document.activeElement === lastElement) {
105
+ firstElement.focus();
106
+ e.preventDefault();
107
+ }
108
+ }
109
+ }
110
+
111
+ element.addEventListener('keydown', handleTabKey);
112
+ firstElement.focus();
113
+
114
+ return () => {
115
+ element.removeEventListener('keydown', handleTabKey);
116
+ };
117
+ }
118
+
56
119
  $effect(() => {
57
120
  if (open) {
121
+ previouslyFocusedElement = document.activeElement as HTMLElement;
58
122
  document.body.style.overflow = 'hidden';
59
123
  document.addEventListener('keydown', handleKeydown);
124
+
125
+ tick().then(() => {
126
+ if (modalElement) {
127
+ trapFocus(modalElement);
128
+ }
129
+ });
60
130
  } else {
61
131
  document.body.style.overflow = '';
62
132
  document.removeEventListener('keydown', handleKeydown);
133
+ if (previouslyFocusedElement) {
134
+ previouslyFocusedElement.focus();
135
+ }
63
136
  }
64
137
 
65
138
  return () => {
@@ -67,18 +140,36 @@
67
140
  document.removeEventListener('keydown', handleKeydown);
68
141
  };
69
142
  });
143
+
144
+ onDestroy(() => {
145
+ if (browser && document.body.style.overflow === 'hidden') {
146
+ document.body.style.overflow = '';
147
+ }
148
+ });
70
149
  </script>
71
150
 
72
151
  {#snippet predefinedHeader()}
73
152
  {#if title || description}
74
- <header class={headerClass}>
153
+ <header class={headerClasses}>
75
154
  <div class="flex-1">
76
155
  {#if title}
77
- <h2 class={titleClass}>{title}</h2>
156
+ <h2
157
+ id="modal-title"
158
+ class={titleClasses}
159
+ data-testid={buildTestId('modal', 'title', testId)}
160
+ >
161
+ {title}
162
+ </h2>
78
163
  {/if}
79
164
  </div>
80
165
  {#if !hideCloseButton}
81
- <button type="button" class={closeClass} onclick={onclose} aria-label="Close">
166
+ <button
167
+ type="button"
168
+ class={closeClass}
169
+ onclick={onclose}
170
+ aria-label="Close"
171
+ data-testid={buildTestId('modal', 'close', testId)}
172
+ >
82
173
  <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
83
174
  <path
84
175
  d="M15 5L5 15M5 5L15 15"
@@ -99,6 +190,7 @@
99
190
  class="text-default-400 hover:bg-default-100 hover:text-default-600 absolute top-4 right-4 z-10 rounded-lg p-2 transition-colors"
100
191
  onclick={onclose}
101
192
  aria-label="Close"
193
+ data-testid={buildTestId('modal', 'close', testId)}
102
194
  >
103
195
  <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
104
196
  <path
@@ -113,17 +205,28 @@
113
205
  {/snippet}
114
206
 
115
207
  {#if open}
116
- <div class={baseClass} role="dialog" aria-modal="true">
208
+ <div
209
+ class={baseClass}
210
+ role="dialog"
211
+ aria-modal="true"
212
+ aria-labelledby={title ? 'modal-title' : undefined}
213
+ bind:this={modalElement}
214
+ >
117
215
  <!-- Backdrop -->
118
216
  <div
119
- class={backdropClass}
217
+ class={backdropClasses}
120
218
  onclick={handleBackdropClick}
121
219
  transition:fade={{ duration: 200 }}
122
220
  role="presentation"
221
+ data-testid={buildTestId('modal', 'backdrop', testId)}
123
222
  ></div>
124
223
 
125
224
  <!-- Modal Container -->
126
- <div class={containerClass} transition:scale={{ duration: 200, easing: quintOut, start: 0.95 }}>
225
+ <div
226
+ class={containerClass}
227
+ transition:scale={{ duration: 200, easing: quintOut, start: 0.95 }}
228
+ data-testid={buildTestId('modal', 'dialog', testId)}
229
+ >
127
230
  <!-- Header -->
128
231
  {#if header}
129
232
  {@render header()}
@@ -133,14 +236,14 @@
133
236
 
134
237
  <!-- Body -->
135
238
  {#if children}
136
- <div class={bodyClass}>
239
+ <div class={bodyClasses} data-testid={buildTestId('modal', 'body', testId)}>
137
240
  {@render children()}
138
241
  </div>
139
242
  {/if}
140
243
 
141
244
  <!-- Footer -->
142
245
  {#if footer}
143
- <footer class={footerClass}>
246
+ <footer class={footerClasses} data-testid={buildTestId('modal', 'footer', testId)}>
144
247
  {@render footer()}
145
248
  </footer>
146
249
  {/if}