@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,170 @@
1
+ import FUserMenu from './FUserMenu.vue';
2
+
3
+ export default {
4
+ title: 'Organisms/FUserMenu',
5
+ component: FUserMenu,
6
+ tags: ['autodocs'],
7
+ argTypes: {
8
+ name: {
9
+ control: 'text',
10
+ description: "Nom de l'utilisateur"
11
+ },
12
+ email: {
13
+ control: 'text',
14
+ description: 'Email'
15
+ },
16
+ avatarSrc: {
17
+ control: 'text',
18
+ description: "URL de l'avatar"
19
+ },
20
+ items: {
21
+ control: 'object',
22
+ description: 'Éléments du menu'
23
+ },
24
+ showLogout: {
25
+ control: 'boolean',
26
+ description: 'Afficher le bouton de déconnexion'
27
+ },
28
+ compact: {
29
+ control: 'boolean',
30
+ description: 'Mode compact'
31
+ }
32
+ }
33
+ };
34
+
35
+ const menuItems = [
36
+ { label: 'Mon profil', href: '/profile', icon: 'user' },
37
+ { label: 'Paramètres', href: '/settings', icon: 'cog' },
38
+ { label: 'Aide', href: '/help', icon: 'question' }
39
+ ];
40
+
41
+ const Template = (args, { argTypes }) => ({
42
+ components: { FUserMenu },
43
+ props: Object.keys(argTypes),
44
+ template: '<FUserMenu v-bind="$props" />'
45
+ });
46
+
47
+ export const Default = Template.bind({});
48
+ Default.args = {
49
+ name: 'Jean Dupont',
50
+ email: 'jean@example.com'
51
+ };
52
+
53
+ export const WithAvatar = Template.bind({});
54
+ WithAvatar.args = {
55
+ name: 'Marie Martin',
56
+ email: 'marie@example.com',
57
+ avatarSrc: 'https://i.pravatar.cc/150?img=5'
58
+ };
59
+
60
+ export const WithMenuItems = Template.bind({});
61
+ WithMenuItems.args = {
62
+ name: 'Pierre Durand',
63
+ email: 'pierre@example.com',
64
+ items: menuItems
65
+ };
66
+
67
+ export const WithLogout = Template.bind({});
68
+ WithLogout.args = {
69
+ name: 'Sophie Petit',
70
+ email: 'sophie@example.com',
71
+ items: menuItems,
72
+ showLogout: true
73
+ };
74
+
75
+ export const Compact = Template.bind({});
76
+ Compact.args = {
77
+ name: 'Lucas Bernard',
78
+ compact: true
79
+ };
80
+
81
+ export const FullFeatured = () => ({
82
+ components: { FUserMenu },
83
+ data() {
84
+ return {
85
+ items: [
86
+ { label: 'Mon profil', href: '/profile', icon: 'user' },
87
+ { label: 'Mes projets', href: '/projects', icon: 'folder' },
88
+ {
89
+ label: 'Notifications',
90
+ href: '/notifications',
91
+ icon: 'bell',
92
+ badge: 3
93
+ },
94
+ { type: 'divider' },
95
+ { label: 'Paramètres', href: '/settings', icon: 'cog' },
96
+ { label: 'Aide', href: '/help', icon: 'question' }
97
+ ]
98
+ };
99
+ },
100
+ methods: {
101
+ handleNavigate(data) {
102
+ alert(`Navigation vers: ${data.item.label}`);
103
+ },
104
+ handleLogout() {
105
+ alert('Déconnexion...');
106
+ }
107
+ },
108
+ template: `
109
+ <FUserMenu
110
+ name="Emma Leroy"
111
+ email="emma.leroy@example.com"
112
+ avatarSrc="https://i.pravatar.cc/150?img=1"
113
+ :items="items"
114
+ showLogout
115
+ @navigate="handleNavigate"
116
+ @logout="handleLogout"
117
+ />
118
+ `
119
+ });
120
+
121
+ export const WithDividers = Template.bind({});
122
+ WithDividers.args = {
123
+ name: 'Thomas Martin',
124
+ email: 'thomas@example.com',
125
+ items: [
126
+ { label: 'Mon compte', href: '/account', icon: 'user' },
127
+ { type: 'divider' },
128
+ { label: 'Tableau de bord', href: '/dashboard', icon: 'home' },
129
+ { label: 'Mes tâches', href: '/tasks', icon: 'check' },
130
+ { type: 'divider' },
131
+ { label: 'Support', href: '/support', icon: 'question' }
132
+ ],
133
+ showLogout: true
134
+ };
135
+
136
+ export const InNavbar = () => ({
137
+ components: { FUserMenu },
138
+ data() {
139
+ return {
140
+ items: menuItems
141
+ };
142
+ },
143
+ template: `
144
+ <div class="flex items-center justify-between p-4 bg-white border-b">
145
+ <span class="font-bold">MonApp</span>
146
+ <FUserMenu
147
+ name="Admin"
148
+ email="admin@example.com"
149
+ :items="items"
150
+ showLogout
151
+ compact
152
+ />
153
+ </div>
154
+ `
155
+ });
156
+
157
+ export const OnlyAvatar = () => ({
158
+ components: { FUserMenu },
159
+ template: `
160
+ <FUserMenu
161
+ name="User"
162
+ avatarSrc="https://i.pravatar.cc/150?img=3"
163
+ compact
164
+ :items="[
165
+ { label: 'Profil', href: '/profile' },
166
+ { label: 'Déconnexion', action: 'logout' }
167
+ ]"
168
+ />
169
+ `
170
+ });
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import FUserMenu from './FUserMenu.vue';
4
+
5
+ describe('FUserMenu', () => {
6
+ it('renders correctly with required props', () => {
7
+ const wrapper = mount(FUserMenu, {
8
+ propsData: { name: 'John Doe' }
9
+ });
10
+ expect(wrapper.exists()).toBe(true);
11
+ });
12
+
13
+ it('displays user name', () => {
14
+ const wrapper = mount(FUserMenu, {
15
+ propsData: { name: 'Jane Smith' }
16
+ });
17
+ expect(wrapper.text()).toContain('Jane Smith');
18
+ });
19
+
20
+ it('displays avatar', () => {
21
+ const wrapper = mount(FUserMenu, {
22
+ propsData: { name: 'John' }
23
+ });
24
+ expect(wrapper.findComponent({ name: 'FAvatar' }).exists()).toBe(true);
25
+ });
26
+
27
+ it('displays email when provided', () => {
28
+ const wrapper = mount(FUserMenu, {
29
+ propsData: { name: 'John', email: 'john@test.com' }
30
+ });
31
+ expect(wrapper.text()).toContain('john@test.com');
32
+ });
33
+
34
+ it('renders menu items', () => {
35
+ const wrapper = mount(FUserMenu, {
36
+ propsData: {
37
+ name: 'John',
38
+ items: [
39
+ { label: 'Profile', href: '/profile' },
40
+ { label: 'Settings', href: '/settings' }
41
+ ]
42
+ }
43
+ });
44
+ expect(wrapper.text()).toContain('Profile');
45
+ });
46
+
47
+ it('renders logout button when showLogout is true', () => {
48
+ const wrapper = mount(FUserMenu, {
49
+ propsData: { name: 'John', showLogout: true }
50
+ });
51
+ const hasLogout =
52
+ wrapper.text().toLowerCase().includes('déconnexion') ||
53
+ wrapper.text().toLowerCase().includes('logout');
54
+ expect(hasLogout).toBe(true);
55
+ });
56
+
57
+ it('emits logout event when logout is clicked', async () => {
58
+ const wrapper = mount(FUserMenu, {
59
+ propsData: { name: 'John', showLogout: true }
60
+ });
61
+ const logoutBtn = wrapper
62
+ .findAllComponents({ name: 'FButton' })
63
+ .filter(
64
+ (b) =>
65
+ b.text().toLowerCase().includes('déconnexion') ||
66
+ b.text().toLowerCase().includes('logout')
67
+ )[0];
68
+ if (logoutBtn) {
69
+ await logoutBtn.trigger('click');
70
+ expect(wrapper.emitted('logout')).toBeTruthy();
71
+ }
72
+ });
73
+
74
+ it('emits navigate event when item is clicked', async () => {
75
+ const wrapper = mount(FUserMenu, {
76
+ propsData: {
77
+ name: 'John',
78
+ items: [{ label: 'Profile', href: '/profile' }]
79
+ }
80
+ });
81
+ const menuItems = wrapper.findAll('[role="menuitem"]');
82
+ if (menuItems.length > 0) {
83
+ await menuItems[0].trigger('click');
84
+ expect(wrapper.emitted('navigate')).toBeTruthy();
85
+ }
86
+ });
87
+
88
+ it('applies compact size', () => {
89
+ const wrapper = mount(FUserMenu, {
90
+ propsData: { name: 'John', compact: true }
91
+ });
92
+ expect(wrapper.exists()).toBe(true);
93
+ });
94
+
95
+ it('renders slot content', () => {
96
+ const wrapper = mount(FUserMenu, {
97
+ propsData: { name: 'John' },
98
+ slots: { default: '<div>Custom content</div>' }
99
+ });
100
+ expect(wrapper.html()).toContain('Custom content');
101
+ });
102
+ });
@@ -0,0 +1,407 @@
1
+ <template>
2
+ <div
3
+ v-if="isLoggedIn"
4
+ class="relative inline-block"
5
+ @keydown.escape="closeMenu"
6
+ >
7
+ <!-- Trigger Button (Avatar + optional username) -->
8
+ <button
9
+ ref="trigger"
10
+ type="button"
11
+ :class="triggerClasses"
12
+ :aria-expanded="isOpen"
13
+ aria-haspopup="menu"
14
+ @click="toggleMenu"
15
+ >
16
+ <f-avatar
17
+ :src="avatarSrc"
18
+ :alt="avatarAlt"
19
+ :initials="avatarInitials"
20
+ :name="computedAvatarName"
21
+ :size="avatarSize"
22
+ :status="avatarStatus"
23
+ />
24
+ <span v-if="showUserName" class="ml-2 font-medium text-neutral-700">
25
+ {{ userName }}
26
+ </span>
27
+ <f-icon
28
+ v-if="showChevron"
29
+ :name="isOpen ? 'chevron-up' : 'chevron-down'"
30
+ size="sm"
31
+ class="ml-1 text-neutral-500"
32
+ />
33
+ </button>
34
+
35
+ <!-- Dropdown Menu -->
36
+ <div
37
+ v-show="isOpen"
38
+ ref="dropdown"
39
+ :class="dropdownClasses"
40
+ role="menu"
41
+ :aria-label="menuAriaLabel"
42
+ >
43
+ <!-- User Info Header -->
44
+ <div v-if="showUserInfo" class="px-4 py-3">
45
+ <f-typography
46
+ variant="body"
47
+ class="font-medium text-neutral-900"
48
+ truncate
49
+ >
50
+ {{ userName }}
51
+ </f-typography>
52
+ <f-typography
53
+ v-if="userEmail"
54
+ variant="caption"
55
+ class="text-neutral-500"
56
+ truncate
57
+ >
58
+ {{ userEmail }}
59
+ </f-typography>
60
+ </div>
61
+
62
+ <f-divider v-if="showUserInfo && hasMenuItems" margin="none" />
63
+
64
+ <!-- Menu Items Slot -->
65
+ <div class="py-1">
66
+ <slot name="menu-items">
67
+ <!-- Default menu items if no slot provided -->
68
+ <template v-for="(item, index) in menuItems">
69
+ <f-divider
70
+ v-if="item.divider"
71
+ :key="`divider-${index}`"
72
+ margin="sm"
73
+ />
74
+ <f-list-item
75
+ v-else
76
+ :key="item.key || `item-${index}`"
77
+ :title="item.label"
78
+ clickable
79
+ :disabled="item.disabled"
80
+ :class="getItemClasses(item)"
81
+ @click="handleItemClick(item, $event)"
82
+ >
83
+ <template v-if="item.icon" #left>
84
+ <f-icon :name="item.icon" size="sm" class="text-neutral-500" />
85
+ </template>
86
+ </f-list-item>
87
+ </template>
88
+ </slot>
89
+ </div>
90
+
91
+ <!-- Logout Section -->
92
+ <template v-if="showLogout">
93
+ <f-divider margin="none" />
94
+ <div class="py-1">
95
+ <f-list-item
96
+ :title="logoutLabel"
97
+ clickable
98
+ class="text-danger-600 hover:bg-danger-50"
99
+ @click="handleLogout"
100
+ >
101
+ <template #left>
102
+ <f-icon name="arrow-right" size="sm" class="text-danger-500" />
103
+ </template>
104
+ </f-list-item>
105
+ </div>
106
+ </template>
107
+ </div>
108
+ </div>
109
+ </template>
110
+
111
+ <script>
112
+ import FAvatar from '../../atoms/FAvatar/FAvatar.vue';
113
+ import FIcon from '../../atoms/FIcon/FIcon.vue';
114
+ import FTypography from '../../atoms/FTypography/FTypography.vue';
115
+ import FDivider from '../../atoms/FDivider/FDivider.vue';
116
+ import FListItem from '../../molecules/FListItem/FListItem.vue';
117
+
118
+ export default {
119
+ name: 'FUserMenu',
120
+ components: {
121
+ FAvatar,
122
+ FIcon,
123
+ FTypography,
124
+ FDivider,
125
+ FListItem
126
+ },
127
+ props: {
128
+ /**
129
+ * Controls whether the menu is shown (based on login state)
130
+ */
131
+ isLoggedIn: {
132
+ type: Boolean,
133
+ default: true
134
+ },
135
+ /**
136
+ * Controls the open state of the dropdown (v-model support)
137
+ */
138
+ value: {
139
+ type: Boolean,
140
+ default: false
141
+ },
142
+ /**
143
+ * User's display name
144
+ */
145
+ userName: {
146
+ type: String,
147
+ default: ''
148
+ },
149
+ /**
150
+ * User's email address
151
+ */
152
+ userEmail: {
153
+ type: String,
154
+ default: ''
155
+ },
156
+ /**
157
+ * Avatar image source URL
158
+ */
159
+ avatarSrc: {
160
+ type: String,
161
+ default: ''
162
+ },
163
+ /**
164
+ * Avatar alt text
165
+ */
166
+ avatarAlt: {
167
+ type: String,
168
+ default: ''
169
+ },
170
+ /**
171
+ * Avatar initials (used when no image)
172
+ */
173
+ avatarInitials: {
174
+ type: String,
175
+ default: ''
176
+ },
177
+ /**
178
+ * Avatar name (used to compute initials if no initials provided)
179
+ */
180
+ avatarName: {
181
+ type: String,
182
+ default: ''
183
+ },
184
+ /**
185
+ * Avatar size
186
+ */
187
+ avatarSize: {
188
+ type: String,
189
+ default: 'md',
190
+ validator: (value) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(value)
191
+ },
192
+ /**
193
+ * Avatar status indicator
194
+ */
195
+ avatarStatus: {
196
+ type: String,
197
+ default: null,
198
+ validator: (value) =>
199
+ [null, 'online', 'busy', 'away', 'offline'].includes(value)
200
+ },
201
+ /**
202
+ * Show username next to avatar in trigger
203
+ */
204
+ showUserName: {
205
+ type: Boolean,
206
+ default: false
207
+ },
208
+ /**
209
+ * Show chevron icon in trigger
210
+ */
211
+ showChevron: {
212
+ type: Boolean,
213
+ default: true
214
+ },
215
+ /**
216
+ * Show user info (name/email) in dropdown header
217
+ */
218
+ showUserInfo: {
219
+ type: Boolean,
220
+ default: true
221
+ },
222
+ /**
223
+ * Array of menu items
224
+ * Each item: { key?: string, label: string, icon?: string, disabled?: boolean, divider?: boolean }
225
+ */
226
+ menuItems: {
227
+ type: Array,
228
+ default: () => []
229
+ },
230
+ /**
231
+ * Show logout button at bottom
232
+ */
233
+ showLogout: {
234
+ type: Boolean,
235
+ default: true
236
+ },
237
+ /**
238
+ * Logout button label
239
+ */
240
+ logoutLabel: {
241
+ type: String,
242
+ default: 'Déconnexion'
243
+ },
244
+ /**
245
+ * Dropdown alignment relative to trigger
246
+ */
247
+ dropdownAlign: {
248
+ type: String,
249
+ default: 'right',
250
+ validator: (value) => ['left', 'right'].includes(value)
251
+ },
252
+ /**
253
+ * Dropdown width
254
+ */
255
+ dropdownWidth: {
256
+ type: String,
257
+ default: 'w-56'
258
+ },
259
+ /**
260
+ * ARIA label for the menu
261
+ */
262
+ menuAriaLabel: {
263
+ type: String,
264
+ default: 'Menu utilisateur'
265
+ }
266
+ },
267
+ data() {
268
+ return {
269
+ internalOpen: false
270
+ };
271
+ },
272
+ computed: {
273
+ /**
274
+ * Computed property for v-model support
275
+ */
276
+ isOpen: {
277
+ get() {
278
+ return this.value !== undefined ? this.value : this.internalOpen;
279
+ },
280
+ set(val) {
281
+ this.internalOpen = val;
282
+ this.$emit('input', val);
283
+ }
284
+ },
285
+ /**
286
+ * Check if there are menu items to display
287
+ */
288
+ hasMenuItems() {
289
+ return this.menuItems.length > 0 || this.$slots['menu-items'];
290
+ },
291
+ /**
292
+ * Computed avatar name (falls back to userName if not provided)
293
+ */
294
+ computedAvatarName() {
295
+ return this.avatarName || this.userName;
296
+ },
297
+ /**
298
+ * Trigger button classes
299
+ */
300
+ triggerClasses() {
301
+ return [
302
+ 'inline-flex items-center',
303
+ 'px-2 py-1 rounded-lg',
304
+ 'transition-colors duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]',
305
+ 'hover:bg-neutral-100',
306
+ 'focus:outline-none focus:ring-2 focus:ring-primary-500/20',
307
+ 'cursor-pointer'
308
+ ].join(' ');
309
+ },
310
+ /**
311
+ * Dropdown container classes
312
+ */
313
+ dropdownClasses() {
314
+ const alignmentClass =
315
+ this.dropdownAlign === 'left' ? 'left-0' : 'right-0';
316
+
317
+ return [
318
+ 'absolute z-50 mt-2',
319
+ alignmentClass,
320
+ this.dropdownWidth,
321
+ 'bg-white rounded-lg shadow-lg',
322
+ 'border border-neutral-200',
323
+ 'overflow-hidden'
324
+ ].join(' ');
325
+ }
326
+ },
327
+ watch: {
328
+ isOpen(newValue) {
329
+ if (newValue) {
330
+ this.$nextTick(() => {
331
+ document.addEventListener('click', this.handleClickOutside);
332
+ });
333
+ } else {
334
+ document.removeEventListener('click', this.handleClickOutside);
335
+ }
336
+ }
337
+ },
338
+ beforeDestroy() {
339
+ document.removeEventListener('click', this.handleClickOutside);
340
+ },
341
+ methods: {
342
+ /**
343
+ * Toggle menu open/close state
344
+ */
345
+ toggleMenu() {
346
+ this.isOpen = !this.isOpen;
347
+ this.$emit('toggle', this.isOpen);
348
+ },
349
+ /**
350
+ * Close the menu
351
+ */
352
+ closeMenu() {
353
+ if (this.isOpen) {
354
+ this.isOpen = false;
355
+ this.$emit('close');
356
+ }
357
+ },
358
+ /**
359
+ * Handle clicks outside the dropdown
360
+ */
361
+ handleClickOutside(event) {
362
+ const dropdown = this.$refs.dropdown;
363
+ const trigger = this.$refs.trigger;
364
+
365
+ if (
366
+ dropdown &&
367
+ trigger &&
368
+ !dropdown.contains(event.target) &&
369
+ !trigger.contains(event.target)
370
+ ) {
371
+ this.closeMenu();
372
+ }
373
+ },
374
+ /**
375
+ * Get classes for menu items based on item config
376
+ */
377
+ getItemClasses(item) {
378
+ const classes = [];
379
+ if (item.danger) {
380
+ classes.push('text-danger-600 hover:bg-danger-50');
381
+ }
382
+ return classes.join(' ');
383
+ },
384
+ /**
385
+ * Handle menu item click
386
+ */
387
+ handleItemClick(item, event) {
388
+ if (item.disabled) return;
389
+
390
+ this.$emit('item-click', { item, event });
391
+ this.$emit('navigate', { item, event });
392
+
393
+ // Close menu after click unless item specifies keepOpen
394
+ if (!item.keepOpen) {
395
+ this.closeMenu();
396
+ }
397
+ },
398
+ /**
399
+ * Handle logout action
400
+ */
401
+ handleLogout() {
402
+ this.$emit('logout');
403
+ this.closeMenu();
404
+ }
405
+ }
406
+ };
407
+ </script>
@@ -0,0 +1,29 @@
1
+ import FActivityFeed from './FActivityFeed/FActivityFeed.vue';
2
+ import FForm from './FForm/FForm.vue';
3
+ import FDataTable from './FDataTable/FDataTable.vue';
4
+ import FDrawer from './FDrawer/FDrawer.vue';
5
+ import FFileUpload from './FFileUpload/FFileUpload.vue';
6
+ import FFilterSidebar from './FFilterSidebar/FFilterSidebar.vue';
7
+ import FNavigationSidebar from './FNavigationSidebar/FNavigationSidebar.vue';
8
+ import FPageHeader from './FPageHeader/FPageHeader.vue';
9
+ import FModal from './FModal/FModal.vue';
10
+ import FUserMenu from './FUserMenu/FUserMenu.vue';
11
+ import FOnboardingStepper from './FOnboardingStepper/FOnboardingStepper.vue';
12
+ import FProfileSection from './FProfileSection/FProfileSection.vue';
13
+ import FToastProvider from './FToastProvider/FToastProvider.vue';
14
+
15
+ export {
16
+ FActivityFeed,
17
+ FForm,
18
+ FDataTable,
19
+ FDrawer,
20
+ FFileUpload,
21
+ FFilterSidebar,
22
+ FNavigationSidebar,
23
+ FPageHeader,
24
+ FModal,
25
+ FUserMenu,
26
+ FOnboardingStepper,
27
+ FProfileSection,
28
+ FToastProvider
29
+ };