@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,161 @@
1
+ import FFilterSidebar from './FFilterSidebar.vue';
2
+ import FButton from '../../atoms/FButton/FButton.vue';
3
+
4
+ export default {
5
+ title: 'Organisms/FFilterSidebar',
6
+ component: FFilterSidebar,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ filters: {
10
+ control: 'object',
11
+ description: 'Configuration des filtres'
12
+ },
13
+ value: {
14
+ control: 'object',
15
+ description: 'Valeurs des filtres sélectionnés'
16
+ },
17
+ title: {
18
+ control: 'text',
19
+ description: 'Titre de la sidebar'
20
+ },
21
+ showClear: {
22
+ control: 'boolean',
23
+ description: 'Afficher le bouton de réinitialisation'
24
+ },
25
+ collapsible: {
26
+ control: 'boolean',
27
+ description: 'Groupes pliables'
28
+ }
29
+ }
30
+ };
31
+
32
+ const sampleFilters = [
33
+ {
34
+ key: 'status',
35
+ label: 'Statut',
36
+ type: 'checkbox',
37
+ options: [
38
+ { label: 'Actif', value: 'active' },
39
+ { label: 'Inactif', value: 'inactive' },
40
+ { label: 'En attente', value: 'pending' }
41
+ ]
42
+ },
43
+ {
44
+ key: 'category',
45
+ label: 'Catégorie',
46
+ type: 'checkbox',
47
+ options: [
48
+ { label: 'Technologie', value: 'tech' },
49
+ { label: 'Design', value: 'design' },
50
+ { label: 'Marketing', value: 'marketing' },
51
+ { label: 'Ventes', value: 'sales' }
52
+ ]
53
+ },
54
+ {
55
+ key: 'priority',
56
+ label: 'Priorité',
57
+ type: 'radio',
58
+ options: [
59
+ { label: 'Haute', value: 'high' },
60
+ { label: 'Moyenne', value: 'medium' },
61
+ { label: 'Basse', value: 'low' }
62
+ ]
63
+ }
64
+ ];
65
+
66
+ const Template = (args, { argTypes }) => ({
67
+ components: { FFilterSidebar },
68
+ props: Object.keys(argTypes),
69
+ data() {
70
+ return { filterValues: args.value || {} };
71
+ },
72
+ template: '<FFilterSidebar v-bind="$props" v-model="filterValues" />'
73
+ });
74
+
75
+ export const Default = Template.bind({});
76
+ Default.args = {
77
+ filters: sampleFilters,
78
+ title: 'Filtres'
79
+ };
80
+
81
+ export const WithClear = Template.bind({});
82
+ WithClear.args = {
83
+ filters: sampleFilters,
84
+ title: 'Filtres',
85
+ showClear: true
86
+ };
87
+
88
+ export const Collapsible = Template.bind({});
89
+ Collapsible.args = {
90
+ filters: sampleFilters,
91
+ title: 'Filtres',
92
+ collapsible: true
93
+ };
94
+
95
+ export const PreselectedValues = Template.bind({});
96
+ PreselectedValues.args = {
97
+ filters: sampleFilters,
98
+ title: 'Filtres',
99
+ value: {
100
+ status: ['active'],
101
+ category: ['tech', 'design'],
102
+ priority: 'high'
103
+ }
104
+ };
105
+
106
+ export const Interactive = () => ({
107
+ components: { FFilterSidebar, FButton },
108
+ data() {
109
+ return {
110
+ filters: sampleFilters,
111
+ values: {}
112
+ };
113
+ },
114
+ methods: {
115
+ handleClear() {
116
+ this.values = {};
117
+ }
118
+ },
119
+ template: `
120
+ <div class="flex gap-8">
121
+ <div class="w-64">
122
+ <FFilterSidebar
123
+ v-model="values"
124
+ :filters="filters"
125
+ title="Filtres"
126
+ showClear
127
+ collapsible
128
+ @clear="handleClear"
129
+ />
130
+ </div>
131
+ <div class="flex-1">
132
+ <h3 class="font-semibold mb-4">Filtres actifs:</h3>
133
+ <pre class="bg-neutral-100 p-4 rounded text-sm">{{ JSON.stringify(values, null, 2) }}</pre>
134
+ </div>
135
+ </div>
136
+ `
137
+ });
138
+
139
+ export const SingleFilter = Template.bind({});
140
+ SingleFilter.args = {
141
+ filters: [sampleFilters[0]],
142
+ title: 'Filtrer par statut'
143
+ };
144
+
145
+ export const RadioOnly = Template.bind({});
146
+ RadioOnly.args = {
147
+ filters: [
148
+ {
149
+ key: 'sort',
150
+ label: 'Trier par',
151
+ type: 'radio',
152
+ options: [
153
+ { label: 'Date (récent)', value: 'date_desc' },
154
+ { label: 'Date (ancien)', value: 'date_asc' },
155
+ { label: 'Nom (A-Z)', value: 'name_asc' },
156
+ { label: 'Nom (Z-A)', value: 'name_desc' }
157
+ ]
158
+ }
159
+ ],
160
+ title: 'Tri'
161
+ };
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import FFilterSidebar from './FFilterSidebar.vue';
4
+
5
+ describe('FFilterSidebar', () => {
6
+ const filters = [
7
+ {
8
+ key: 'status',
9
+ label: 'Status',
10
+ type: 'checkbox',
11
+ options: [
12
+ { label: 'Active', value: 'active' },
13
+ { label: 'Inactive', value: 'inactive' }
14
+ ]
15
+ }
16
+ ];
17
+
18
+ it('renders correctly with required props', () => {
19
+ const wrapper = mount(FFilterSidebar, {
20
+ propsData: { filters }
21
+ });
22
+ expect(wrapper.exists()).toBe(true);
23
+ });
24
+
25
+ it('displays filter labels', () => {
26
+ const wrapper = mount(FFilterSidebar, {
27
+ propsData: { filters }
28
+ });
29
+ expect(wrapper.text()).toContain('Status');
30
+ });
31
+
32
+ it('displays filter options', () => {
33
+ const wrapper = mount(FFilterSidebar, {
34
+ propsData: { filters }
35
+ });
36
+ expect(wrapper.text()).toContain('Active');
37
+ expect(wrapper.text()).toContain('Inactive');
38
+ });
39
+
40
+ it('shows title when provided', () => {
41
+ const wrapper = mount(FFilterSidebar, {
42
+ propsData: { filters, title: 'Filtres' }
43
+ });
44
+ expect(wrapper.text()).toContain('Filtres');
45
+ });
46
+
47
+ it('shows clear button when showClear is true', () => {
48
+ const wrapper = mount(FFilterSidebar, {
49
+ propsData: { filters, showClear: true }
50
+ });
51
+ expect(wrapper.findComponent({ name: 'FButton' }).exists()).toBe(true);
52
+ });
53
+
54
+ it('emits update event when filter changes', async () => {
55
+ const wrapper = mount(FFilterSidebar, {
56
+ propsData: { filters, value: {} }
57
+ });
58
+ const checkbox = wrapper.find('input[type="checkbox"]');
59
+ if (checkbox.exists()) {
60
+ await checkbox.setChecked(true);
61
+ expect(
62
+ wrapper.emitted('input') || wrapper.emitted('update')
63
+ ).toBeTruthy();
64
+ }
65
+ });
66
+
67
+ it('emits clear event when clear button is clicked', async () => {
68
+ const wrapper = mount(FFilterSidebar, {
69
+ propsData: { filters, showClear: true }
70
+ });
71
+ const clearBtn = wrapper
72
+ .findAllComponents({ name: 'FButton' })
73
+ .filter(
74
+ (b) =>
75
+ b.text().toLowerCase().includes('effacer') ||
76
+ b.text().toLowerCase().includes('réinitialiser')
77
+ )[0];
78
+ if (clearBtn) {
79
+ await clearBtn.trigger('click');
80
+ expect(wrapper.emitted('clear')).toBeTruthy();
81
+ }
82
+ });
83
+
84
+ it('renders accordion items for each filter group', () => {
85
+ const wrapper = mount(FFilterSidebar, {
86
+ propsData: { filters }
87
+ });
88
+ expect(wrapper.findComponent({ name: 'FAccordionItem' }).exists()).toBe(
89
+ true
90
+ );
91
+ });
92
+ });
@@ -0,0 +1,458 @@
1
+ <template>
2
+ <aside
3
+ :class="sidebarClasses"
4
+ :style="getSidebarStyle()"
5
+ :aria-hidden="!isOpen"
6
+ role="complementary"
7
+ aria-label="Barre latérale de filtres"
8
+ >
9
+ <!-- Mobile Overlay -->
10
+ <div
11
+ v-if="isMobile && isOpen"
12
+ class="fixed inset-0 bg-black bg-opacity-50 z-40"
13
+ @click="closeSidebar"
14
+ ></div>
15
+
16
+ <!-- Sidebar Content -->
17
+ <div :class="contentClasses">
18
+ <!-- Header -->
19
+ <div
20
+ class="flex items-center justify-between p-4 border-b border-neutral-200"
21
+ >
22
+ <f-typography variant="h5">
23
+ {{ title }}
24
+ </f-typography>
25
+ <f-button
26
+ v-if="closable"
27
+ variant="ghost"
28
+ size="small"
29
+ @click="closeSidebar"
30
+ >
31
+ <f-icon name="close" size="sm" />
32
+ <span class="sr-only">Fermer les filtres</span>
33
+ </f-button>
34
+ </div>
35
+
36
+ <!-- Filter Form -->
37
+ <f-form class="flex-1 overflow-y-auto" @submit="handleSubmit">
38
+ <div class="p-4 space-y-4">
39
+ <slot>
40
+ <!-- Default filter groups using accordion -->
41
+ <f-accordion-item
42
+ v-for="(group, index) in filterGroups"
43
+ :key="group.id || index"
44
+ :title="group.title"
45
+ :default-open="group.defaultOpen !== false"
46
+ >
47
+ <div class="space-y-3">
48
+ <!-- Checkbox filters -->
49
+ <template v-if="group.type === 'checkbox'">
50
+ <f-checkbox
51
+ v-for="option in group.options"
52
+ :key="option.value"
53
+ :label="option.label"
54
+ :checked="isChecked(group.name, option.value)"
55
+ :disabled="option.disabled"
56
+ @change="
57
+ handleCheckboxChange(group.name, option.value, $event)
58
+ "
59
+ />
60
+ </template>
61
+
62
+ <!-- Radio filters -->
63
+ <template v-else-if="group.type === 'radio'">
64
+ <f-radio
65
+ v-for="option in group.options"
66
+ :key="option.value"
67
+ :label="option.label"
68
+ :value="option.value"
69
+ :name="group.name"
70
+ :model-value="getFilterValue(group.name)"
71
+ :disabled="option.disabled"
72
+ @change="handleRadioChange(group.name, $event)"
73
+ />
74
+ </template>
75
+
76
+ <!-- Toggle filters -->
77
+ <template v-else-if="group.type === 'toggle'">
78
+ <f-toggle
79
+ v-for="option in group.options"
80
+ :key="option.value"
81
+ :label="option.label"
82
+ :value="getToggleValue(group.name, option.value)"
83
+ :disabled="option.disabled"
84
+ @input="
85
+ handleToggleChange(group.name, option.value, $event)
86
+ "
87
+ />
88
+ </template>
89
+
90
+ <!-- Text/Input filters -->
91
+ <template v-else-if="group.type === 'text'">
92
+ <f-form-field
93
+ v-for="option in group.options"
94
+ :key="option.value"
95
+ :label="option.label"
96
+ :placeholder="option.placeholder"
97
+ :value="getFilterValue(group.name + '.' + option.value)"
98
+ @input="handleInputChange(group.name, option.value, $event)"
99
+ />
100
+ </template>
101
+ </div>
102
+ </f-accordion-item>
103
+ </slot>
104
+ </div>
105
+
106
+ <!-- Actions -->
107
+ <template #actions>
108
+ <div class="p-4 border-t border-neutral-200 space-y-2">
109
+ <f-button type="submit" variant="primary" block>
110
+ {{ applyLabel }}
111
+ </f-button>
112
+ <f-button
113
+ type="button"
114
+ variant="outline"
115
+ block
116
+ @click="handleReset"
117
+ >
118
+ {{ resetLabel }}
119
+ </f-button>
120
+ </div>
121
+ </template>
122
+ </f-form>
123
+ </div>
124
+ </aside>
125
+ </template>
126
+
127
+ <script>
128
+ import FTypography from '../../atoms/FTypography/FTypography.vue';
129
+ import FButton from '../../atoms/FButton/FButton.vue';
130
+ import FIcon from '../../atoms/FIcon/FIcon.vue';
131
+ import FCheckbox from '../../atoms/FCheckbox/FCheckbox.vue';
132
+ import FRadio from '../../atoms/FRadio/FRadio.vue';
133
+ import FToggle from '../../atoms/FToggle/FToggle.vue';
134
+ import FForm from '../FForm/FForm.vue';
135
+ import FAccordionItem from '../../molecules/FAccordionItem/FAccordionItem.vue';
136
+ import FFormField from '../../molecules/FFormField/FFormField.vue';
137
+
138
+ export default {
139
+ name: 'FFilterSidebar',
140
+ components: {
141
+ FTypography,
142
+ FButton,
143
+ FIcon,
144
+ FCheckbox,
145
+ FRadio,
146
+ FToggle,
147
+ FForm,
148
+ FAccordionItem,
149
+ FFormField
150
+ },
151
+ props: {
152
+ /**
153
+ * Controls the visibility of the sidebar.
154
+ * Use v-model for two-way binding.
155
+ */
156
+ value: {
157
+ type: Boolean,
158
+ default: true
159
+ },
160
+ /**
161
+ * Title displayed in the sidebar header
162
+ */
163
+ title: {
164
+ type: String,
165
+ default: 'Filtres'
166
+ },
167
+ /**
168
+ * Label for the apply button
169
+ */
170
+ applyLabel: {
171
+ type: String,
172
+ default: 'Appliquer les filtres'
173
+ },
174
+ /**
175
+ * Label for the reset button
176
+ */
177
+ resetLabel: {
178
+ type: String,
179
+ default: 'Réinitialiser'
180
+ },
181
+ /**
182
+ * Show the close button in the header
183
+ */
184
+ closable: {
185
+ type: Boolean,
186
+ default: true
187
+ },
188
+ /**
189
+ * Sidebar position
190
+ */
191
+ position: {
192
+ type: String,
193
+ default: 'left',
194
+ validator: (value) => ['left', 'right'].includes(value)
195
+ },
196
+ /**
197
+ * Width of the sidebar
198
+ */
199
+ width: {
200
+ type: String,
201
+ default: '280px'
202
+ },
203
+ /**
204
+ * Filter groups configuration
205
+ * Each group has: { id, title, name, type, options, defaultOpen }
206
+ * type: 'checkbox' | 'radio' | 'toggle' | 'text'
207
+ * options: [{ value, label, placeholder?, disabled? }]
208
+ */
209
+ filterGroups: {
210
+ type: Array,
211
+ default: () => []
212
+ },
213
+ /**
214
+ * Initial filter values
215
+ * Object with group names as keys
216
+ */
217
+ filters: {
218
+ type: Object,
219
+ default: () => ({})
220
+ },
221
+ /**
222
+ * Enable mobile-responsive overlay behavior
223
+ */
224
+ mobileBreakpoint: {
225
+ type: Number,
226
+ default: 768
227
+ }
228
+ },
229
+ data() {
230
+ return {
231
+ localFilters: {},
232
+ isMobile: false
233
+ };
234
+ },
235
+ computed: {
236
+ /**
237
+ * Computed property for v-model support
238
+ */
239
+ isOpen: {
240
+ get() {
241
+ return this.value;
242
+ },
243
+ set(val) {
244
+ this.$emit('input', val);
245
+ }
246
+ },
247
+ /**
248
+ * Sidebar wrapper classes
249
+ */
250
+ sidebarClasses() {
251
+ const baseClasses = 'flex flex-col bg-white';
252
+
253
+ const positionClasses = this.isMobile
254
+ ? this.getMobilePositionClasses()
255
+ : this.getDesktopPositionClasses();
256
+
257
+ const visibilityClasses = this.isOpen
258
+ ? 'translate-x-0'
259
+ : this.position === 'left'
260
+ ? '-translate-x-full'
261
+ : 'translate-x-full';
262
+
263
+ return [
264
+ baseClasses,
265
+ positionClasses,
266
+ this.isMobile ? visibilityClasses : '',
267
+ 'transition-transform duration-[var(--transition-duration-slow)] ease-[var(--transition-easing-emphasized)]'
268
+ ]
269
+ .filter(Boolean)
270
+ .join(' ');
271
+ },
272
+ /**
273
+ * Content wrapper classes
274
+ */
275
+ contentClasses() {
276
+ const baseClasses = 'flex flex-col h-full bg-white border-neutral-200';
277
+ const borderClasses = this.position === 'left' ? 'border-r' : 'border-l';
278
+
279
+ return [baseClasses, borderClasses].filter(Boolean).join(' ');
280
+ }
281
+ },
282
+ watch: {
283
+ filters: {
284
+ immediate: true,
285
+ deep: true,
286
+ handler(newFilters) {
287
+ this.localFilters = JSON.parse(JSON.stringify(newFilters || {}));
288
+ }
289
+ },
290
+ isOpen(newValue) {
291
+ if (this.isMobile) {
292
+ if (newValue) {
293
+ document.body.style.overflow = 'hidden';
294
+ } else {
295
+ document.body.style.overflow = '';
296
+ }
297
+ }
298
+ }
299
+ },
300
+ mounted() {
301
+ this.checkMobile();
302
+ window.addEventListener('resize', this.checkMobile);
303
+ },
304
+ beforeDestroy() {
305
+ window.removeEventListener('resize', this.checkMobile);
306
+ document.body.style.overflow = '';
307
+ },
308
+ methods: {
309
+ /**
310
+ * Get desktop position classes
311
+ */
312
+ getDesktopPositionClasses() {
313
+ return 'relative flex-shrink-0';
314
+ },
315
+ /**
316
+ * Get sidebar inline styles
317
+ */
318
+ getSidebarStyle() {
319
+ if (!this.isMobile) {
320
+ return { width: this.width };
321
+ }
322
+ return {};
323
+ },
324
+ /**
325
+ * Get mobile position classes
326
+ */
327
+ getMobilePositionClasses() {
328
+ const positionClasses = this.position === 'left' ? 'left-0' : 'right-0';
329
+
330
+ return `fixed top-0 ${positionClasses} bottom-0 z-50 w-80 max-w-full`;
331
+ },
332
+ /**
333
+ * Check if viewport is mobile
334
+ */
335
+ checkMobile() {
336
+ this.isMobile = window.innerWidth < this.mobileBreakpoint;
337
+ },
338
+ /**
339
+ * Close the sidebar
340
+ */
341
+ closeSidebar() {
342
+ this.isOpen = false;
343
+ this.$emit('close');
344
+ },
345
+ /**
346
+ * Get filter value for a specific key
347
+ */
348
+ getFilterValue(key) {
349
+ const keys = key.split('.');
350
+ let value = this.localFilters;
351
+ for (const k of keys) {
352
+ if (value === undefined || value === null) return null;
353
+ value = value[k];
354
+ }
355
+ return value;
356
+ },
357
+ /**
358
+ * Check if a checkbox option is checked
359
+ */
360
+ isChecked(groupName, optionValue) {
361
+ const groupValues = this.localFilters[groupName];
362
+ if (Array.isArray(groupValues)) {
363
+ return groupValues.includes(optionValue);
364
+ }
365
+ return false;
366
+ },
367
+ /**
368
+ * Get toggle value for a specific option
369
+ */
370
+ getToggleValue(groupName, optionValue) {
371
+ const groupValues = this.localFilters[groupName];
372
+ if (typeof groupValues === 'object' && groupValues !== null) {
373
+ return Boolean(groupValues[optionValue]);
374
+ }
375
+ return false;
376
+ },
377
+ /**
378
+ * Handle checkbox change
379
+ */
380
+ handleCheckboxChange(groupName, optionValue, checked) {
381
+ if (!this.localFilters[groupName]) {
382
+ this.$set(this.localFilters, groupName, []);
383
+ }
384
+
385
+ const values = [...this.localFilters[groupName]];
386
+ const index = values.indexOf(optionValue);
387
+
388
+ if (checked && index === -1) {
389
+ values.push(optionValue);
390
+ } else if (!checked && index !== -1) {
391
+ values.splice(index, 1);
392
+ }
393
+
394
+ this.$set(this.localFilters, groupName, values);
395
+ this.$emit('filter-change', { group: groupName, value: values });
396
+ },
397
+ /**
398
+ * Handle radio change
399
+ */
400
+ handleRadioChange(groupName, value) {
401
+ this.$set(this.localFilters, groupName, value);
402
+ this.$emit('filter-change', { group: groupName, value });
403
+ },
404
+ /**
405
+ * Handle toggle change
406
+ */
407
+ handleToggleChange(groupName, optionValue, checked) {
408
+ if (!this.localFilters[groupName]) {
409
+ this.$set(this.localFilters, groupName, {});
410
+ }
411
+
412
+ this.$set(this.localFilters[groupName], optionValue, checked);
413
+ this.$emit('filter-change', {
414
+ group: groupName,
415
+ option: optionValue,
416
+ value: checked
417
+ });
418
+ },
419
+ /**
420
+ * Handle input change
421
+ */
422
+ handleInputChange(groupName, optionValue, value) {
423
+ const key = `${groupName}.${optionValue}`;
424
+ const keys = key.split('.');
425
+ let current = this.localFilters;
426
+
427
+ for (let i = 0; i < keys.length - 1; i++) {
428
+ if (!current[keys[i]]) {
429
+ this.$set(current, keys[i], {});
430
+ }
431
+ current = current[keys[i]];
432
+ }
433
+
434
+ this.$set(current, keys[keys.length - 1], value);
435
+ this.$emit('filter-change', {
436
+ group: groupName,
437
+ option: optionValue,
438
+ value
439
+ });
440
+ },
441
+ /**
442
+ * Handle form submission
443
+ */
444
+ handleSubmit() {
445
+ this.$emit('apply', this.localFilters);
446
+ this.$emit('submit', this.localFilters);
447
+ },
448
+ /**
449
+ * Handle reset action
450
+ */
451
+ handleReset() {
452
+ this.localFilters = {};
453
+ this.$emit('reset');
454
+ this.$emit('update:filters', {});
455
+ }
456
+ }
457
+ };
458
+ </script>