@pyreweb/fabric 1.2.6

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 (210) hide show
  1. package/README.md +119 -0
  2. package/dist/fabric.cjs.js +18109 -0
  3. package/dist/fabric.css +2180 -0
  4. package/dist/fabric.esm.js +18062 -0
  5. package/dist/fabric.min.js +18112 -0
  6. package/dist/types/components/atoms/FAvatar/FAvatar.test.d.ts +1 -0
  7. package/dist/types/components/atoms/FBadge/FBadge.test.d.ts +1 -0
  8. package/dist/types/components/atoms/FButton/FButton.test.d.ts +1 -0
  9. package/dist/types/components/atoms/FCheckbox/FCheckbox.test.d.ts +1 -0
  10. package/dist/types/components/atoms/FDivider/FDivider.test.d.ts +1 -0
  11. package/dist/types/components/atoms/FIcon/FIcon.test.d.ts +1 -0
  12. package/dist/types/components/atoms/FInput/FInput.test.d.ts +1 -0
  13. package/dist/types/components/atoms/FLoader/FLoader.test.d.ts +1 -0
  14. package/dist/types/components/atoms/FRadio/FRadio.test.d.ts +1 -0
  15. package/dist/types/components/atoms/FTextarea/FTextarea.test.d.ts +1 -0
  16. package/dist/types/components/atoms/FToggle/FToggle.test.d.ts +1 -0
  17. package/dist/types/components/atoms/FTypography/FTypography.test.d.ts +1 -0
  18. package/dist/types/components/atoms/index.d.ts +13 -0
  19. package/dist/types/components/molecules/FAccordionItem/FAccordionItem.test.d.ts +1 -0
  20. package/dist/types/components/molecules/FAlert/FAlert.test.d.ts +1 -0
  21. package/dist/types/components/molecules/FBreadcrumb/FBreadcrumb.test.d.ts +1 -0
  22. package/dist/types/components/molecules/FButtonGroup/FButtonGroup.test.d.ts +1 -0
  23. package/dist/types/components/molecules/FCard/FCard.test.d.ts +1 -0
  24. package/dist/types/components/molecules/FDatePicker/FDatePicker.test.d.ts +1 -0
  25. package/dist/types/components/molecules/FEmptyState/FEmptyState.test.d.ts +1 -0
  26. package/dist/types/components/molecules/FFilePreview/FFilePreview.test.d.ts +1 -0
  27. package/dist/types/components/molecules/FFormField/FFormField.test.d.ts +1 -0
  28. package/dist/types/components/molecules/FListItem/FListItem.test.d.ts +1 -0
  29. package/dist/types/components/molecules/FPagination/FPagination.test.d.ts +1 -0
  30. package/dist/types/components/molecules/FSearchBar/FSearchBar.test.d.ts +1 -0
  31. package/dist/types/components/molecules/FSelect/FSelect.test.d.ts +1 -0
  32. package/dist/types/components/molecules/FStatCard/FStatCard.test.d.ts +1 -0
  33. package/dist/types/components/molecules/FTabs/FTabs.test.d.ts +1 -0
  34. package/dist/types/components/molecules/FToast/FToast.test.d.ts +1 -0
  35. package/dist/types/components/molecules/index.d.ts +18 -0
  36. package/dist/types/components/organisms/FActivityFeed/FActivityFeed.test.d.ts +1 -0
  37. package/dist/types/components/organisms/FDataTable/FDataTable.test.d.ts +1 -0
  38. package/dist/types/components/organisms/FDrawer/FDrawer.test.d.ts +1 -0
  39. package/dist/types/components/organisms/FFileUpload/FFileUpload.test.d.ts +1 -0
  40. package/dist/types/components/organisms/FFilterSidebar/FFilterSidebar.test.d.ts +1 -0
  41. package/dist/types/components/organisms/FForm/FForm.test.d.ts +1 -0
  42. package/dist/types/components/organisms/FModal/FModal.test.d.ts +1 -0
  43. package/dist/types/components/organisms/FNavigationSidebar/FNavigationSidebar.test.d.ts +1 -0
  44. package/dist/types/components/organisms/FOnboardingStepper/FOnboardingStepper.test.d.ts +1 -0
  45. package/dist/types/components/organisms/FOnboardingStepper/FStepperProgress.test.d.ts +1 -0
  46. package/dist/types/components/organisms/FPageHeader/FPageHeader.test.d.ts +1 -0
  47. package/dist/types/components/organisms/FProfileSection/FProfileSection.test.d.ts +1 -0
  48. package/dist/types/components/organisms/FToastProvider/FToastProvider.test.d.ts +1 -0
  49. package/dist/types/components/organisms/FUserMenu/FUserMenu.test.d.ts +1 -0
  50. package/dist/types/components/organisms/index.d.ts +14 -0
  51. package/dist/types/components/utils/FThemeProvider.test.d.ts +1 -0
  52. package/dist/types/components/utils/index.d.ts +2 -0
  53. package/dist/types/components.d.ts +602 -0
  54. package/dist/types/composables/index.d.ts +12 -0
  55. package/dist/types/composables/useDataTableState.d.ts +106 -0
  56. package/dist/types/composables/useDataTableState.test.d.ts +1 -0
  57. package/dist/types/composables/useFormValidation.d.ts +49 -0
  58. package/dist/types/composables/useFormValidation.test.d.ts +1 -0
  59. package/dist/types/composables/useSidebarState.d.ts +65 -0
  60. package/dist/types/composables/useSidebarState.test.d.ts +1 -0
  61. package/dist/types/index.d.ts +19 -0
  62. package/dist/types/types.d.ts +529 -0
  63. package/package.json +100 -0
  64. package/src/components/atoms/FAvatar/FAvatar.stories.js +100 -0
  65. package/src/components/atoms/FAvatar/FAvatar.test.ts +95 -0
  66. package/src/components/atoms/FAvatar/FAvatar.vue +190 -0
  67. package/src/components/atoms/FBadge/FBadge.stories.js +129 -0
  68. package/src/components/atoms/FBadge/FBadge.test.ts +93 -0
  69. package/src/components/atoms/FBadge/FBadge.vue +103 -0
  70. package/src/components/atoms/FButton/FButton.stories.js +122 -0
  71. package/src/components/atoms/FButton/FButton.test.ts +98 -0
  72. package/src/components/atoms/FButton/FButton.vue +147 -0
  73. package/src/components/atoms/FCheckbox/FCheckbox.stories.js +96 -0
  74. package/src/components/atoms/FCheckbox/FCheckbox.test.ts +64 -0
  75. package/src/components/atoms/FCheckbox/FCheckbox.vue +76 -0
  76. package/src/components/atoms/FDivider/FDivider.stories.js +104 -0
  77. package/src/components/atoms/FDivider/FDivider.test.ts +80 -0
  78. package/src/components/atoms/FDivider/FDivider.vue +117 -0
  79. package/src/components/atoms/FIcon/FIcon.stories.js +189 -0
  80. package/src/components/atoms/FIcon/FIcon.test.ts +99 -0
  81. package/src/components/atoms/FIcon/FIcon.vue +192 -0
  82. package/src/components/atoms/FInput/FInput.stories.js +119 -0
  83. package/src/components/atoms/FInput/FInput.test.ts +79 -0
  84. package/src/components/atoms/FInput/FInput.vue +88 -0
  85. package/src/components/atoms/FLoader/FLoader.stories.js +109 -0
  86. package/src/components/atoms/FLoader/FLoader.test.ts +66 -0
  87. package/src/components/atoms/FLoader/FLoader.vue +97 -0
  88. package/src/components/atoms/FRadio/FRadio.stories.js +105 -0
  89. package/src/components/atoms/FRadio/FRadio.test.ts +75 -0
  90. package/src/components/atoms/FRadio/FRadio.vue +119 -0
  91. package/src/components/atoms/FTextarea/FTextarea.stories.js +126 -0
  92. package/src/components/atoms/FTextarea/FTextarea.test.ts +94 -0
  93. package/src/components/atoms/FTextarea/FTextarea.vue +156 -0
  94. package/src/components/atoms/FToggle/FToggle.stories.js +108 -0
  95. package/src/components/atoms/FToggle/FToggle.test.ts +96 -0
  96. package/src/components/atoms/FToggle/FToggle.vue +123 -0
  97. package/src/components/atoms/FTypography/FTypography.stories.js +127 -0
  98. package/src/components/atoms/FTypography/FTypography.test.ts +93 -0
  99. package/src/components/atoms/FTypography/FTypography.vue +78 -0
  100. package/src/components/atoms/index.ts +27 -0
  101. package/src/components/molecules/FAccordionItem/FAccordionItem.stories.js +71 -0
  102. package/src/components/molecules/FAccordionItem/FAccordionItem.test.ts +61 -0
  103. package/src/components/molecules/FAccordionItem/FAccordionItem.vue +105 -0
  104. package/src/components/molecules/FAlert/FAlert.stories.js +87 -0
  105. package/src/components/molecules/FAlert/FAlert.test.ts +59 -0
  106. package/src/components/molecules/FAlert/FAlert.vue +108 -0
  107. package/src/components/molecules/FBreadcrumb/FBreadcrumb.stories.js +90 -0
  108. package/src/components/molecules/FBreadcrumb/FBreadcrumb.test.ts +76 -0
  109. package/src/components/molecules/FBreadcrumb/FBreadcrumb.vue +117 -0
  110. package/src/components/molecules/FButtonGroup/FButtonGroup.stories.js +82 -0
  111. package/src/components/molecules/FButtonGroup/FButtonGroup.test.ts +44 -0
  112. package/src/components/molecules/FButtonGroup/FButtonGroup.vue +31 -0
  113. package/src/components/molecules/FCard/FCard.stories.js +136 -0
  114. package/src/components/molecules/FCard/FCard.test.ts +87 -0
  115. package/src/components/molecules/FCard/FCard.vue +75 -0
  116. package/src/components/molecules/FDatePicker/FDatePicker.stories.js +305 -0
  117. package/src/components/molecules/FDatePicker/FDatePicker.test.ts +282 -0
  118. package/src/components/molecules/FDatePicker/FDatePicker.vue +750 -0
  119. package/src/components/molecules/FEmptyState/FEmptyState.stories.js +98 -0
  120. package/src/components/molecules/FEmptyState/FEmptyState.test.ts +82 -0
  121. package/src/components/molecules/FEmptyState/FEmptyState.vue +89 -0
  122. package/src/components/molecules/FFilePreview/FFilePreview.stories.js +130 -0
  123. package/src/components/molecules/FFilePreview/FFilePreview.test.ts +70 -0
  124. package/src/components/molecules/FFilePreview/FFilePreview.vue +125 -0
  125. package/src/components/molecules/FFormField/FFormField.stories.js +149 -0
  126. package/src/components/molecules/FFormField/FFormField.test.ts +85 -0
  127. package/src/components/molecules/FFormField/FFormField.vue +107 -0
  128. package/src/components/molecules/FListItem/FListItem.stories.js +158 -0
  129. package/src/components/molecules/FListItem/FListItem.test.ts +93 -0
  130. package/src/components/molecules/FListItem/FListItem.vue +113 -0
  131. package/src/components/molecules/FPagination/FPagination.stories.js +132 -0
  132. package/src/components/molecules/FPagination/FPagination.test.ts +79 -0
  133. package/src/components/molecules/FPagination/FPagination.vue +206 -0
  134. package/src/components/molecules/FSearchBar/FSearchBar.stories.js +129 -0
  135. package/src/components/molecules/FSearchBar/FSearchBar.test.ts +81 -0
  136. package/src/components/molecules/FSearchBar/FSearchBar.vue +180 -0
  137. package/src/components/molecules/FSelect/FSelect.stories.js +333 -0
  138. package/src/components/molecules/FSelect/FSelect.test.ts +478 -0
  139. package/src/components/molecules/FSelect/FSelect.vue +551 -0
  140. package/src/components/molecules/FStatCard/FStatCard.stories.js +144 -0
  141. package/src/components/molecules/FStatCard/FStatCard.test.ts +78 -0
  142. package/src/components/molecules/FStatCard/FStatCard.vue +106 -0
  143. package/src/components/molecules/FTabs/FTab.vue +63 -0
  144. package/src/components/molecules/FTabs/FTabs.stories.js +277 -0
  145. package/src/components/molecules/FTabs/FTabs.test.ts +264 -0
  146. package/src/components/molecules/FTabs/FTabs.vue +273 -0
  147. package/src/components/molecules/FToast/FToast.stories.js +150 -0
  148. package/src/components/molecules/FToast/FToast.test.ts +157 -0
  149. package/src/components/molecules/FToast/FToast.vue +283 -0
  150. package/src/components/molecules/index.ts +37 -0
  151. package/src/components/organisms/FActivityFeed/FActivityFeed.stories.js +217 -0
  152. package/src/components/organisms/FActivityFeed/FActivityFeed.test.ts +134 -0
  153. package/src/components/organisms/FActivityFeed/FActivityFeed.vue +589 -0
  154. package/src/components/organisms/FDataTable/FDataTable.stories.js +370 -0
  155. package/src/components/organisms/FDataTable/FDataTable.test.ts +248 -0
  156. package/src/components/organisms/FDataTable/FDataTable.vue +808 -0
  157. package/src/components/organisms/FDrawer/FDrawer.stories.js +296 -0
  158. package/src/components/organisms/FDrawer/FDrawer.test.ts +142 -0
  159. package/src/components/organisms/FDrawer/FDrawer.vue +303 -0
  160. package/src/components/organisms/FFileUpload/FFileUpload.stories.js +162 -0
  161. package/src/components/organisms/FFileUpload/FFileUpload.test.ts +103 -0
  162. package/src/components/organisms/FFileUpload/FFileUpload.vue +616 -0
  163. package/src/components/organisms/FFilterSidebar/FFilterSidebar.stories.js +161 -0
  164. package/src/components/organisms/FFilterSidebar/FFilterSidebar.test.ts +92 -0
  165. package/src/components/organisms/FFilterSidebar/FFilterSidebar.vue +458 -0
  166. package/src/components/organisms/FForm/FForm.stories.js +270 -0
  167. package/src/components/organisms/FForm/FForm.test.ts +63 -0
  168. package/src/components/organisms/FForm/FForm.vue +19 -0
  169. package/src/components/organisms/FModal/FModal.stories.js +227 -0
  170. package/src/components/organisms/FModal/FModal.test.ts +181 -0
  171. package/src/components/organisms/FModal/FModal.vue +319 -0
  172. package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.stories.js +176 -0
  173. package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.test.ts +95 -0
  174. package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.vue +577 -0
  175. package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.stories.js +197 -0
  176. package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.test.ts +114 -0
  177. package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.vue +212 -0
  178. package/src/components/organisms/FOnboardingStepper/FStepperProgress.stories.js +122 -0
  179. package/src/components/organisms/FOnboardingStepper/FStepperProgress.test.ts +130 -0
  180. package/src/components/organisms/FOnboardingStepper/FStepperProgress.vue +146 -0
  181. package/src/components/organisms/FPageHeader/FPageHeader.stories.js +142 -0
  182. package/src/components/organisms/FPageHeader/FPageHeader.test.ts +83 -0
  183. package/src/components/organisms/FPageHeader/FPageHeader.vue +241 -0
  184. package/src/components/organisms/FProfileSection/FProfileSection.stories.js +190 -0
  185. package/src/components/organisms/FProfileSection/FProfileSection.test.ts +85 -0
  186. package/src/components/organisms/FProfileSection/FProfileSection.vue +562 -0
  187. package/src/components/organisms/FToastProvider/FToastProvider.stories.js +290 -0
  188. package/src/components/organisms/FToastProvider/FToastProvider.test.ts +215 -0
  189. package/src/components/organisms/FToastProvider/FToastProvider.vue +214 -0
  190. package/src/components/organisms/FUserMenu/FUserMenu.stories.js +170 -0
  191. package/src/components/organisms/FUserMenu/FUserMenu.test.ts +102 -0
  192. package/src/components/organisms/FUserMenu/FUserMenu.vue +407 -0
  193. package/src/components/organisms/index.ts +29 -0
  194. package/src/components/utils/FThemeProvider.stories.js +236 -0
  195. package/src/components/utils/FThemeProvider.test.ts +244 -0
  196. package/src/components/utils/FThemeProvider.vue +191 -0
  197. package/src/components/utils/index.ts +3 -0
  198. package/src/components.d.ts +602 -0
  199. package/src/composables/README.md +233 -0
  200. package/src/composables/index.ts +25 -0
  201. package/src/composables/useDataTableState.test.ts +378 -0
  202. package/src/composables/useDataTableState.ts +361 -0
  203. package/src/composables/useFormValidation.test.ts +198 -0
  204. package/src/composables/useFormValidation.ts +178 -0
  205. package/src/composables/useSidebarState.test.ts +307 -0
  206. package/src/composables/useSidebarState.ts +201 -0
  207. package/src/env.d.ts +14 -0
  208. package/src/index.ts +167 -0
  209. package/src/styles/tailwind.css +173 -0
  210. package/src/types.ts +740 -0
@@ -0,0 +1,264 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount, createLocalVue } from '@vue/test-utils';
3
+ import FTabs from './FTabs.vue';
4
+ import FTab from './FTab.vue';
5
+
6
+ const localVue = createLocalVue();
7
+ localVue.component('FTab', FTab);
8
+
9
+ describe('FTabs', () => {
10
+ it('renders correctly with default props', () => {
11
+ const wrapper = mount(FTabs, {
12
+ localVue,
13
+ slots: {
14
+ default: `
15
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
16
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
17
+ `
18
+ }
19
+ });
20
+ expect(wrapper.find('[role="tablist"]').exists()).toBe(true);
21
+ });
22
+
23
+ it('renders all tab buttons', () => {
24
+ const wrapper = mount(FTabs, {
25
+ localVue,
26
+ slots: {
27
+ default: `
28
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
29
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
30
+ <FTab name="tab3" label="Tab 3" disabled>Content 3</FTab>
31
+ `
32
+ }
33
+ });
34
+ wrapper.vm.$nextTick(() => {
35
+ const tabs = wrapper.findAll('[role="tab"]');
36
+ expect(tabs.length).toBe(3);
37
+ });
38
+ });
39
+
40
+ it('activates first enabled tab by default', async () => {
41
+ const wrapper = mount(FTabs, {
42
+ localVue,
43
+ slots: {
44
+ default: `
45
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
46
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
47
+ `
48
+ }
49
+ });
50
+ await wrapper.vm.$nextTick();
51
+ await wrapper.vm.$nextTick();
52
+ await wrapper.vm.$nextTick();
53
+ await new Promise((resolve) => setTimeout(resolve, 10));
54
+ await wrapper.vm.$nextTick();
55
+
56
+ // Verify tabs are registered
57
+ expect(wrapper.vm.tabItems.length).toBeGreaterThan(0);
58
+
59
+ const tabs = wrapper.findAll('[role="tab"]');
60
+ expect(tabs.length).toBeGreaterThan(0);
61
+
62
+ // Check that at least one tab is selected
63
+ const selectedTabs = tabs.wrappers.filter(
64
+ (tab) => tab.attributes('aria-selected') === 'true'
65
+ );
66
+ expect(selectedTabs.length).toBeGreaterThan(0);
67
+ });
68
+
69
+ it('changes active tab on click', async () => {
70
+ const wrapper = mount(FTabs, {
71
+ localVue,
72
+ slots: {
73
+ default: `
74
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
75
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
76
+ `
77
+ }
78
+ });
79
+ await wrapper.vm.$nextTick();
80
+ await wrapper.vm.$nextTick();
81
+ const tabs = wrapper.findAll('[role="tab"]');
82
+ if (tabs.length > 1) {
83
+ await tabs.at(1).trigger('click');
84
+ expect(wrapper.emitted('input')).toBeTruthy();
85
+ expect(wrapper.emitted('change')).toBeTruthy();
86
+ }
87
+ });
88
+
89
+ it('does not activate disabled tab on click', async () => {
90
+ const wrapper = mount(FTabs, {
91
+ localVue,
92
+ slots: {
93
+ default: `
94
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
95
+ <FTab name="tab2" label="Tab 2" disabled>Content 2</FTab>
96
+ `
97
+ }
98
+ });
99
+ await wrapper.vm.$nextTick();
100
+ await wrapper.vm.$nextTick();
101
+ const tabs = wrapper.findAll('[role="tab"]');
102
+ if (tabs.length > 1) {
103
+ const initialEmitCount = wrapper.emitted('input')
104
+ ? wrapper.emitted('input').length
105
+ : 0;
106
+ await tabs.at(1).trigger('click');
107
+ const finalEmitCount = wrapper.emitted('input')
108
+ ? wrapper.emitted('input').length
109
+ : 0;
110
+ expect(finalEmitCount).toBe(initialEmitCount);
111
+ }
112
+ });
113
+
114
+ it('supports v-model binding', async () => {
115
+ const wrapperWithModel = mount(FTabs, {
116
+ localVue,
117
+ propsData: {
118
+ value: 'tab2'
119
+ },
120
+ slots: {
121
+ default: `
122
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
123
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
124
+ `
125
+ }
126
+ });
127
+ await wrapperWithModel.vm.$nextTick();
128
+ const tabs = wrapperWithModel.findAll('[role="tab"]');
129
+ expect(tabs.at(1).attributes('aria-selected')).toBe('true');
130
+ });
131
+
132
+ it('navigates with arrow keys', async () => {
133
+ const wrapper = mount(FTabs, {
134
+ localVue,
135
+ slots: {
136
+ default: `
137
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
138
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
139
+ `
140
+ }
141
+ });
142
+ await wrapper.vm.$nextTick();
143
+ await wrapper.vm.$nextTick();
144
+ const tabs = wrapper.findAll('[role="tab"]');
145
+ if (tabs.length > 0) {
146
+ await tabs.at(0).trigger('keydown', { key: 'ArrowRight' });
147
+ expect(wrapper.emitted('change')).toBeTruthy();
148
+ }
149
+ });
150
+
151
+ it('navigates to first tab with Home key', async () => {
152
+ const wrapper = mount(FTabs, {
153
+ localVue,
154
+ propsData: {
155
+ value: 'tab2'
156
+ },
157
+ slots: {
158
+ default: `
159
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
160
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
161
+ `
162
+ }
163
+ });
164
+ await wrapper.vm.$nextTick();
165
+ await wrapper.vm.$nextTick();
166
+ const tabs = wrapper.findAll('[role="tab"]');
167
+ await tabs.at(1).trigger('keydown', { key: 'Home' });
168
+ expect(wrapper.emitted('change')[0][0]).toBe('tab1');
169
+ });
170
+
171
+ it('navigates to last tab with End key', async () => {
172
+ const wrapper = mount(FTabs, {
173
+ localVue,
174
+ slots: {
175
+ default: `
176
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
177
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
178
+ `
179
+ }
180
+ });
181
+ await wrapper.vm.$nextTick();
182
+ await wrapper.vm.$nextTick();
183
+ const tabs = wrapper.findAll('[role="tab"]');
184
+ if (tabs.length > 0) {
185
+ await tabs.at(0).trigger('keydown', { key: 'End' });
186
+ expect(wrapper.emitted('change')).toBeTruthy();
187
+ }
188
+ });
189
+
190
+ it('applies correct variant classes', () => {
191
+ const variants = ['default', 'pills', 'underline'];
192
+ variants.forEach((variant) => {
193
+ const variantWrapper = mount(FTabs, {
194
+ localVue,
195
+ propsData: { variant },
196
+ slots: {
197
+ default: '<FTab name="tab1" label="Tab 1">Content</FTab>'
198
+ }
199
+ });
200
+ expect(variantWrapper.find('[role="tablist"]').exists()).toBe(true);
201
+ });
202
+ });
203
+
204
+ it('has correct ARIA attributes', async () => {
205
+ const wrapper = mount(FTabs, {
206
+ localVue,
207
+ slots: {
208
+ default: `
209
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
210
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
211
+ `
212
+ }
213
+ });
214
+ await wrapper.vm.$nextTick();
215
+ const tabList = wrapper.find('[role="tablist"]');
216
+ expect(tabList.attributes('aria-label')).toBeDefined();
217
+
218
+ const tabs = wrapper.findAll('[role="tab"]');
219
+ tabs.wrappers.forEach((tab) => {
220
+ expect(tab.attributes('aria-controls')).toBeDefined();
221
+ expect(tab.attributes('aria-selected')).toBeDefined();
222
+ });
223
+ });
224
+
225
+ it('sets correct tabindex for active and inactive tabs', async () => {
226
+ const wrapper = mount(FTabs, {
227
+ localVue,
228
+ propsData: {
229
+ value: 'tab1'
230
+ },
231
+ slots: {
232
+ default: `
233
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
234
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
235
+ `
236
+ }
237
+ });
238
+ await wrapper.vm.$nextTick();
239
+ await wrapper.vm.$nextTick(); // Wait for mounted nextTick to complete
240
+ const tabs = wrapper.findAll('[role="tab"]');
241
+ expect(tabs.at(0).attributes('tabindex')).toBe('0');
242
+ expect(tabs.at(1).attributes('tabindex')).toBe('-1');
243
+ });
244
+
245
+ it('displays tab panel content when tab is active', async () => {
246
+ const wrapper = mount(FTabs, {
247
+ localVue,
248
+ propsData: {
249
+ value: 'tab1'
250
+ },
251
+ slots: {
252
+ default: `
253
+ <FTab name="tab1" label="Tab 1">Content 1</FTab>
254
+ <FTab name="tab2" label="Tab 2">Content 2</FTab>
255
+ `
256
+ }
257
+ });
258
+ await wrapper.vm.$nextTick();
259
+ await wrapper.vm.$nextTick(); // Wait for mounted nextTick to complete
260
+ const panels = wrapper.findAll('[role="tabpanel"]');
261
+ expect(panels.at(0).isVisible()).toBe(true);
262
+ expect(panels.at(1).isVisible()).toBe(false);
263
+ });
264
+ });
@@ -0,0 +1,273 @@
1
+ <template>
2
+ <div class="f-tabs">
3
+ <!-- Tab buttons -->
4
+ <div :class="tabListClasses" role="tablist" :aria-label="ariaLabel">
5
+ <button
6
+ v-for="tab in tabItems"
7
+ :id="getTabId(tab.name)"
8
+ :key="tab.name"
9
+ :ref="`tab-${tab.name}`"
10
+ role="tab"
11
+ :aria-selected="activeTabName === tab.name ? 'true' : 'false'"
12
+ :aria-controls="getPanelId(tab.name)"
13
+ :disabled="tab.disabled"
14
+ :class="getTabButtonClasses(tab)"
15
+ :tabindex="activeTabName === tab.name ? 0 : -1"
16
+ @click="handleTabClick(tab.name)"
17
+ @keydown="handleKeydown($event, tab.name)"
18
+ >
19
+ {{ tab.label }}
20
+ </button>
21
+ </div>
22
+
23
+ <!-- Tab panels (content) -->
24
+ <div class="mt-4">
25
+ <slot />
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script>
31
+ let idCounter = 0;
32
+
33
+ export default {
34
+ name: 'FTabs',
35
+ provide() {
36
+ return {
37
+ tabsProvider: {
38
+ registerTab: this.registerTab,
39
+ unregisterTab: this.unregisterTab,
40
+ isActive: this.isTabActive,
41
+ getTabId: this.getTabId,
42
+ getPanelId: this.getPanelId
43
+ }
44
+ };
45
+ },
46
+ props: {
47
+ /**
48
+ * Currently active tab name (v-model support)
49
+ */
50
+ value: {
51
+ type: String,
52
+ default: ''
53
+ },
54
+ /**
55
+ * Visual variant of the tabs
56
+ */
57
+ variant: {
58
+ type: String,
59
+ default: 'default',
60
+ validator: (value) => ['default', 'pills', 'underline'].includes(value)
61
+ },
62
+ /**
63
+ * Position of the tab buttons
64
+ */
65
+ position: {
66
+ type: String,
67
+ default: 'top',
68
+ validator: (value) => ['top', 'bottom'].includes(value)
69
+ },
70
+ /**
71
+ * Accessible label for the tab list
72
+ */
73
+ ariaLabel: {
74
+ type: String,
75
+ default: 'Onglets'
76
+ }
77
+ },
78
+ data() {
79
+ return {
80
+ tabItems: [],
81
+ uid: idCounter++,
82
+ initialTabSet: false,
83
+ internalActiveTab: ''
84
+ };
85
+ },
86
+ computed: {
87
+ activeTabName: {
88
+ get() {
89
+ // Use value prop if provided, otherwise use internal state
90
+ return this.value !== '' ? this.value : this.internalActiveTab;
91
+ },
92
+ set(val) {
93
+ this.internalActiveTab = val;
94
+ this.$emit('input', val);
95
+ }
96
+ },
97
+ tabListClasses() {
98
+ const baseClasses = 'flex gap-1';
99
+ const variantClasses = {
100
+ default: 'border-b border-neutral-200',
101
+ pills: '',
102
+ underline: 'border-b border-neutral-200'
103
+ };
104
+ const positionClasses = {
105
+ top: '',
106
+ bottom: 'order-2 mt-4'
107
+ };
108
+
109
+ return [
110
+ baseClasses,
111
+ variantClasses[this.variant],
112
+ positionClasses[this.position]
113
+ ]
114
+ .filter(Boolean)
115
+ .join(' ');
116
+ }
117
+ },
118
+ watch: {
119
+ tabItems: {
120
+ handler() {
121
+ // Set first tab as active if no value provided and not yet set
122
+ if (
123
+ !this.initialTabSet &&
124
+ !this.activeTabName &&
125
+ this.tabItems.length > 0
126
+ ) {
127
+ const firstEnabledTab = this.tabItems.find((tab) => !tab.disabled);
128
+ if (firstEnabledTab) {
129
+ this.activeTabName = firstEnabledTab.name;
130
+ this.initialTabSet = true;
131
+ }
132
+ }
133
+ },
134
+ immediate: true
135
+ }
136
+ },
137
+ methods: {
138
+ /**
139
+ * Register a tab from FTab component
140
+ */
141
+ registerTab(tab) {
142
+ const exists = this.tabItems.find((t) => t.name === tab.name);
143
+ if (!exists) {
144
+ this.tabItems.push(tab);
145
+ }
146
+ // Set first non-disabled tab as active if no value provided
147
+ if (!this.activeTabName) {
148
+ const firstEnabledTab = this.tabItems.find((t) => !t.disabled);
149
+ if (firstEnabledTab) {
150
+ this.activeTabName = firstEnabledTab.name;
151
+ }
152
+ }
153
+ },
154
+ /**
155
+ * Unregister a tab from FTab component
156
+ */
157
+ unregisterTab(name) {
158
+ const index = this.tabItems.findIndex((t) => t.name === name);
159
+ if (index > -1) {
160
+ this.tabItems.splice(index, 1);
161
+ }
162
+ },
163
+ /**
164
+ * Check if a tab is active
165
+ */
166
+ isTabActive(name) {
167
+ return this.activeTabName === name;
168
+ },
169
+ /**
170
+ * Generate unique tab button ID
171
+ */
172
+ getTabId(name) {
173
+ return `f-tab-${this.uid}-${name}`;
174
+ },
175
+ /**
176
+ * Generate unique tab panel ID
177
+ */
178
+ getPanelId(name) {
179
+ return `f-tabpanel-${this.uid}-${name}`;
180
+ },
181
+ /**
182
+ * Get classes for tab button based on state
183
+ */
184
+ getTabButtonClasses(tab) {
185
+ const isActive = this.activeTabName === tab.name;
186
+ const baseClasses =
187
+ 'px-4 py-2 font-medium text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2';
188
+
189
+ const roundingClasses = this.variant === 'pills' ? '' : 'rounded-t-md';
190
+
191
+ const variantClasses = {
192
+ default: isActive
193
+ ? 'text-primary-600 border-b-2 border-primary-600 -mb-px'
194
+ : 'text-neutral-600 hover:text-neutral-800 hover:border-neutral-300 border-b-2 border-transparent -mb-px',
195
+ pills: isActive
196
+ ? 'bg-primary-100 text-primary-700 rounded-lg'
197
+ : 'text-neutral-600 hover:bg-neutral-100 hover:text-neutral-800 rounded-lg',
198
+ underline: isActive
199
+ ? 'text-primary-600 border-b-2 border-primary-600 -mb-px'
200
+ : 'text-neutral-600 hover:text-neutral-800 border-b-2 border-transparent -mb-px'
201
+ };
202
+
203
+ const disabledClasses = tab.disabled
204
+ ? 'opacity-50 cursor-not-allowed'
205
+ : 'cursor-pointer';
206
+
207
+ return [
208
+ baseClasses,
209
+ roundingClasses,
210
+ variantClasses[this.variant],
211
+ disabledClasses
212
+ ]
213
+ .filter(Boolean)
214
+ .join(' ');
215
+ },
216
+ /**
217
+ * Handle tab click
218
+ */
219
+ handleTabClick(name) {
220
+ const tab = this.tabItems.find((t) => t.name === name);
221
+ if (tab && !tab.disabled) {
222
+ this.activeTabName = name;
223
+ this.$emit('change', name);
224
+ }
225
+ },
226
+ /**
227
+ * Handle keyboard navigation
228
+ */
229
+ handleKeydown(event, currentName) {
230
+ const enabledTabs = this.tabItems.filter((tab) => !tab.disabled);
231
+ const currentIndex = enabledTabs.findIndex(
232
+ (tab) => tab.name === currentName
233
+ );
234
+
235
+ let nextIndex = currentIndex;
236
+
237
+ switch (event.key) {
238
+ case 'ArrowRight':
239
+ event.preventDefault();
240
+ nextIndex = (currentIndex + 1) % enabledTabs.length;
241
+ break;
242
+ case 'ArrowLeft':
243
+ event.preventDefault();
244
+ nextIndex =
245
+ currentIndex === 0 ? enabledTabs.length - 1 : currentIndex - 1;
246
+ break;
247
+ case 'Home':
248
+ event.preventDefault();
249
+ nextIndex = 0;
250
+ break;
251
+ case 'End':
252
+ event.preventDefault();
253
+ nextIndex = enabledTabs.length - 1;
254
+ break;
255
+ default:
256
+ return;
257
+ }
258
+
259
+ const nextTab = enabledTabs[nextIndex];
260
+ if (nextTab) {
261
+ this.activeTabName = nextTab.name;
262
+ this.$emit('change', nextTab.name);
263
+ this.$nextTick(() => {
264
+ const tabButton = this.$refs[`tab-${nextTab.name}`];
265
+ if (tabButton && tabButton[0]) {
266
+ tabButton[0].focus();
267
+ }
268
+ });
269
+ }
270
+ }
271
+ }
272
+ };
273
+ </script>
@@ -0,0 +1,150 @@
1
+ import FToast from './FToast.vue';
2
+
3
+ export default {
4
+ title: 'Molecules/FToast',
5
+ component: FToast,
6
+ tags: ['autodocs'],
7
+ argTypes: {
8
+ variant: {
9
+ control: { type: 'select' },
10
+ options: ['success', 'error', 'info', 'warning'],
11
+ description: 'Type de notification'
12
+ },
13
+ title: {
14
+ control: 'text',
15
+ description: 'Titre de la notification'
16
+ },
17
+ message: {
18
+ control: 'text',
19
+ description: 'Message de la notification'
20
+ },
21
+ closable: {
22
+ control: 'boolean',
23
+ description: 'Afficher le bouton de fermeture'
24
+ },
25
+ duration: {
26
+ control: 'number',
27
+ description: "Durée d'affichage en millisecondes (0 = permanent)"
28
+ },
29
+ position: {
30
+ control: { type: 'select' },
31
+ options: [
32
+ 'top-left',
33
+ 'top-center',
34
+ 'top-right',
35
+ 'bottom-left',
36
+ 'bottom-center',
37
+ 'bottom-right'
38
+ ],
39
+ description: 'Position du toast'
40
+ }
41
+ }
42
+ };
43
+
44
+ const Template = (args, { argTypes }) => ({
45
+ components: { FToast },
46
+ props: Object.keys(argTypes),
47
+ template: '<FToast v-bind="$props" @close="onClose" />',
48
+ methods: {
49
+ onClose() {
50
+ console.log('Toast fermé');
51
+ }
52
+ }
53
+ });
54
+
55
+ export const Success = Template.bind({});
56
+ Success.args = {
57
+ variant: 'success',
58
+ title: 'Succès',
59
+ message: 'Votre action a été effectuée avec succès.',
60
+ duration: 0
61
+ };
62
+
63
+ export const Error = Template.bind({});
64
+ Error.args = {
65
+ variant: 'error',
66
+ title: 'Erreur',
67
+ message: "Une erreur s'est produite. Veuillez réessayer.",
68
+ duration: 0
69
+ };
70
+
71
+ export const Info = Template.bind({});
72
+ Info.args = {
73
+ variant: 'info',
74
+ title: 'Information',
75
+ message: 'Voici une information importante à prendre en compte.',
76
+ duration: 0
77
+ };
78
+
79
+ export const Warning = Template.bind({});
80
+ Warning.args = {
81
+ variant: 'warning',
82
+ title: 'Avertissement',
83
+ message: 'Veuillez vérifier vos informations avant de continuer.',
84
+ duration: 0
85
+ };
86
+
87
+ export const WithoutTitle = Template.bind({});
88
+ WithoutTitle.args = {
89
+ variant: 'info',
90
+ message: 'Notification simple sans titre.',
91
+ duration: 0
92
+ };
93
+
94
+ export const NotClosable = Template.bind({});
95
+ NotClosable.args = {
96
+ variant: 'info',
97
+ title: 'Notification permanente',
98
+ message: 'Cette notification ne peut pas être fermée.',
99
+ closable: false,
100
+ duration: 0
101
+ };
102
+
103
+ export const WithSlotContent = () => ({
104
+ components: { FToast },
105
+ template: `
106
+ <FToast variant="info" title="Information" :duration="0">
107
+ <p>Contenu personnalisé avec <a href="#" class="underline">un lien</a> cliquable.</p>
108
+ </FToast>
109
+ `
110
+ });
111
+
112
+ export const AllVariants = () => ({
113
+ components: { FToast },
114
+ template: `
115
+ <div class="flex flex-col gap-4 p-4">
116
+ <FToast variant="success" title="Succès" message="Opération réussie." :duration="0" />
117
+ <FToast variant="error" title="Erreur" message="Une erreur s'est produite." :duration="0" />
118
+ <FToast variant="info" title="Information" message="Information importante." :duration="0" />
119
+ <FToast variant="warning" title="Avertissement" message="Veuillez vérifier." :duration="0" />
120
+ </div>
121
+ `
122
+ });
123
+
124
+ export const WithAutoClose = () => ({
125
+ components: { FToast },
126
+ data() {
127
+ return {
128
+ showToast: true
129
+ };
130
+ },
131
+ template: `
132
+ <div class="p-4">
133
+ <FToast
134
+ v-if="showToast"
135
+ variant="success"
136
+ title="Auto-fermeture"
137
+ message="Ce toast se fermera automatiquement dans 5 secondes."
138
+ :duration="5000"
139
+ @close="showToast = false"
140
+ />
141
+ <button
142
+ v-if="!showToast"
143
+ @click="showToast = true"
144
+ class="px-4 py-2 bg-primary-500 text-white rounded"
145
+ >
146
+ Afficher à nouveau
147
+ </button>
148
+ </div>
149
+ `
150
+ });