@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,551 @@
1
+ <template>
2
+ <div :class="containerClasses" @keydown.escape="closeDropdown">
3
+ <!-- Trigger Button -->
4
+ <button
5
+ :id="triggerId"
6
+ ref="trigger"
7
+ type="button"
8
+ :class="triggerClasses"
9
+ :disabled="disabled"
10
+ :aria-expanded="String(isOpen)"
11
+ :aria-haspopup="'listbox'"
12
+ :aria-labelledby="labelId"
13
+ @click="toggleDropdown"
14
+ @keydown.down.prevent="openDropdown"
15
+ @keydown.up.prevent="openDropdown"
16
+ >
17
+ <span :class="valueClasses">{{ displayValue }}</span>
18
+ <f-icon
19
+ :name="isOpen ? 'chevron-up' : 'chevron-down'"
20
+ size="sm"
21
+ :class="iconClasses"
22
+ />
23
+ </button>
24
+
25
+ <!-- Dropdown -->
26
+ <div
27
+ v-show="isOpen"
28
+ ref="dropdown"
29
+ :class="dropdownClasses"
30
+ role="listbox"
31
+ :aria-labelledby="labelId"
32
+ :aria-multiselectable="String(multiple)"
33
+ >
34
+ <!-- Search Input -->
35
+ <div v-if="searchable" class="p-2 border-b border-neutral-200">
36
+ <div class="relative">
37
+ <f-icon
38
+ name="search"
39
+ size="sm"
40
+ class="absolute left-2.5 top-1/2 -translate-y-1/2 text-neutral-400 pointer-events-none"
41
+ />
42
+ <input
43
+ ref="searchInput"
44
+ v-model="searchQuery"
45
+ type="text"
46
+ :class="searchInputClasses"
47
+ :placeholder="searchPlaceholder"
48
+ @keydown.down.prevent="handleKeyboardNavigation('down')"
49
+ @keydown.up.prevent="handleKeyboardNavigation('up')"
50
+ @keydown.enter.prevent="handleEnterKey"
51
+ @keydown.escape.prevent="closeDropdown"
52
+ />
53
+ </div>
54
+ </div>
55
+
56
+ <!-- Loading State -->
57
+ <div v-if="loading" class="p-4 text-center">
58
+ <f-loader size="sm" />
59
+ <p class="mt-2 text-sm text-neutral-500">{{ loadingText }}</p>
60
+ </div>
61
+
62
+ <!-- Options List -->
63
+ <div
64
+ v-else-if="filteredOptions.length > 0"
65
+ ref="optionsList"
66
+ :class="optionsListClasses"
67
+ >
68
+ <div
69
+ v-for="(option, index) in filteredOptions"
70
+ :key="getOptionKey(option, index)"
71
+ :ref="`option-${index}`"
72
+ role="option"
73
+ :aria-selected="String(isSelected(option))"
74
+ :class="getOptionClasses(option, index)"
75
+ @click="handleOptionClick(option)"
76
+ @mouseenter="focusedIndex = index"
77
+ >
78
+ <f-checkbox
79
+ v-if="multiple"
80
+ :value="isSelected(option)"
81
+ :disabled="isDisabled(option)"
82
+ tabindex="-1"
83
+ class="pointer-events-none"
84
+ />
85
+ <span :class="optionLabelClasses">{{ getOptionLabel(option) }}</span>
86
+ <f-icon
87
+ v-if="!multiple && isSelected(option)"
88
+ name="check"
89
+ size="sm"
90
+ class="ml-auto text-primary-500"
91
+ />
92
+ </div>
93
+ </div>
94
+
95
+ <!-- Empty State -->
96
+ <div v-else class="p-4 text-center">
97
+ <f-icon name="file" size="lg" class="text-neutral-300 mb-2" />
98
+ <p class="text-sm text-neutral-500">{{ emptyText }}</p>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </template>
103
+
104
+ <script>
105
+ import FIcon from '../../atoms/FIcon/FIcon.vue';
106
+ import FLoader from '../../atoms/FLoader/FLoader.vue';
107
+ import FCheckbox from '../../atoms/FCheckbox/FCheckbox.vue';
108
+
109
+ let selectCounter = 0;
110
+
111
+ export default {
112
+ name: 'FSelect',
113
+ components: {
114
+ FIcon,
115
+ FLoader,
116
+ FCheckbox
117
+ },
118
+ props: {
119
+ /**
120
+ * Valeur sélectionnée (v-model)
121
+ * Pour sélection simple: string | number | object
122
+ * Pour sélection multiple: Array
123
+ */
124
+ value: {
125
+ type: [String, Number, Object, Array],
126
+ default: null
127
+ },
128
+ /**
129
+ * Liste des options
130
+ */
131
+ options: {
132
+ type: Array,
133
+ default: () => []
134
+ },
135
+ /**
136
+ * Clé pour identifier une option (si options sont des objets)
137
+ */
138
+ optionKey: {
139
+ type: String,
140
+ default: 'value'
141
+ },
142
+ /**
143
+ * Clé pour le label d'une option (si options sont des objets)
144
+ */
145
+ optionLabel: {
146
+ type: String,
147
+ default: 'label'
148
+ },
149
+ /**
150
+ * Clé pour désactiver une option (si options sont des objets)
151
+ */
152
+ optionDisabled: {
153
+ type: String,
154
+ default: 'disabled'
155
+ },
156
+ /**
157
+ * Texte affiché quand aucune valeur n'est sélectionnée
158
+ */
159
+ placeholder: {
160
+ type: String,
161
+ default: 'Sélectionner...'
162
+ },
163
+ /**
164
+ * Taille du composant
165
+ */
166
+ size: {
167
+ type: String,
168
+ default: 'medium',
169
+ validator: (value) => ['small', 'medium', 'large'].includes(value)
170
+ },
171
+ /**
172
+ * Activer la sélection multiple
173
+ */
174
+ multiple: {
175
+ type: Boolean,
176
+ default: false
177
+ },
178
+ /**
179
+ * Activer le champ de recherche
180
+ */
181
+ searchable: {
182
+ type: Boolean,
183
+ default: false
184
+ },
185
+ /**
186
+ * Placeholder du champ de recherche
187
+ */
188
+ searchPlaceholder: {
189
+ type: String,
190
+ default: 'Rechercher...'
191
+ },
192
+ /**
193
+ * Texte affiché quand aucune option ne correspond à la recherche
194
+ */
195
+ emptyText: {
196
+ type: String,
197
+ default: 'Aucune option trouvée'
198
+ },
199
+ /**
200
+ * État de chargement (pour options asynchrones)
201
+ */
202
+ loading: {
203
+ type: Boolean,
204
+ default: false
205
+ },
206
+ /**
207
+ * Texte affiché pendant le chargement
208
+ */
209
+ loadingText: {
210
+ type: String,
211
+ default: 'Chargement...'
212
+ },
213
+ /**
214
+ * État désactivé
215
+ */
216
+ disabled: {
217
+ type: Boolean,
218
+ default: false
219
+ },
220
+ /**
221
+ * État d'erreur
222
+ */
223
+ error: {
224
+ type: Boolean,
225
+ default: false
226
+ },
227
+ /**
228
+ * ID du label associé (pour accessibilité)
229
+ */
230
+ labelId: {
231
+ type: String,
232
+ default: null
233
+ },
234
+ /**
235
+ * Fonction de filtrage personnalisée
236
+ */
237
+ filterMethod: {
238
+ type: Function,
239
+ default: null
240
+ }
241
+ },
242
+ data() {
243
+ return {
244
+ uniqueId: ++selectCounter,
245
+ isOpen: false,
246
+ searchQuery: '',
247
+ focusedIndex: -1
248
+ };
249
+ },
250
+ computed: {
251
+ triggerId() {
252
+ return `fselect-trigger-${this.uniqueId}`;
253
+ },
254
+ containerClasses() {
255
+ return 'relative inline-block w-full';
256
+ },
257
+ triggerClasses() {
258
+ const baseClasses =
259
+ 'flex items-center justify-between w-full font-sans border rounded box-border focus:outline-none focus:ring-2 text-left';
260
+
261
+ const transitionClasses =
262
+ 'transition-all duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]';
263
+
264
+ const sizeClasses = {
265
+ small: 'py-1.5 px-2.5 text-xs',
266
+ medium: 'py-2.5 px-3.5 text-sm',
267
+ large: 'py-3.5 px-4.5 text-base'
268
+ };
269
+
270
+ const stateClasses = this.error
271
+ ? 'border-danger-500 focus:border-danger-500 focus:ring-danger-500/20'
272
+ : 'border-neutral-300 focus:border-primary-500 focus:ring-primary-500/20';
273
+
274
+ const disabledClasses = this.disabled
275
+ ? 'bg-neutral-100 cursor-not-allowed opacity-70'
276
+ : 'bg-white cursor-pointer hover:border-neutral-400';
277
+
278
+ return [
279
+ baseClasses,
280
+ transitionClasses,
281
+ sizeClasses[this.size],
282
+ stateClasses,
283
+ disabledClasses
284
+ ]
285
+ .filter(Boolean)
286
+ .join(' ');
287
+ },
288
+ valueClasses() {
289
+ const baseClasses = 'flex-1 truncate';
290
+ const placeholderClasses = !this.hasValue
291
+ ? 'text-neutral-400'
292
+ : 'text-neutral-900';
293
+ return [baseClasses, placeholderClasses].join(' ');
294
+ },
295
+ iconClasses() {
296
+ const baseClasses = 'ml-2 flex-shrink-0';
297
+ const transitionClasses =
298
+ 'transition-transform duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]';
299
+ const colorClasses = this.disabled
300
+ ? 'text-neutral-400'
301
+ : 'text-neutral-500';
302
+ return [baseClasses, transitionClasses, colorClasses].join(' ');
303
+ },
304
+ dropdownClasses() {
305
+ return 'absolute z-50 w-full mt-1 bg-white border border-neutral-200 rounded shadow-lg max-h-60 overflow-hidden';
306
+ },
307
+ searchInputClasses() {
308
+ return 'w-full pl-8 pr-2.5 py-1.5 text-sm border border-neutral-300 rounded focus:outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20';
309
+ },
310
+ optionsListClasses() {
311
+ return 'max-h-48 overflow-y-auto';
312
+ },
313
+ optionLabelClasses() {
314
+ return 'flex-1';
315
+ },
316
+ hasValue() {
317
+ if (this.multiple) {
318
+ return Array.isArray(this.value) && this.value.length > 0;
319
+ }
320
+ return (
321
+ this.value !== null && this.value !== undefined && this.value !== ''
322
+ );
323
+ },
324
+ displayValue() {
325
+ if (!this.hasValue) {
326
+ return this.placeholder;
327
+ }
328
+
329
+ if (this.multiple && Array.isArray(this.value)) {
330
+ const labels = this.value.map((val) => {
331
+ const option = this.options.find(
332
+ (opt) => this.getOptionValue(opt) === this.getOptionValue(val)
333
+ );
334
+ return option
335
+ ? this.getOptionLabel(option)
336
+ : this.getOptionLabel(val);
337
+ });
338
+ return labels.join(', ');
339
+ }
340
+
341
+ const selectedOption = this.options.find(
342
+ (opt) => this.getOptionValue(opt) === this.getOptionValue(this.value)
343
+ );
344
+ return selectedOption
345
+ ? this.getOptionLabel(selectedOption)
346
+ : this.getOptionLabel(this.value);
347
+ },
348
+ filteredOptions() {
349
+ if (!this.searchQuery) {
350
+ return this.options;
351
+ }
352
+
353
+ if (this.filterMethod) {
354
+ return this.filterMethod(this.searchQuery, this.options);
355
+ }
356
+
357
+ const query = this.searchQuery.toLowerCase();
358
+ return this.options.filter((option) => {
359
+ const label = this.getOptionLabel(option).toLowerCase();
360
+ return label.includes(query);
361
+ });
362
+ }
363
+ },
364
+ watch: {
365
+ isOpen(newValue) {
366
+ if (newValue) {
367
+ this.$nextTick(() => {
368
+ if (this.searchable && this.$refs.searchInput) {
369
+ this.$refs.searchInput.focus();
370
+ }
371
+ this.setupClickOutside();
372
+ });
373
+ this.$emit('open');
374
+ } else {
375
+ this.searchQuery = '';
376
+ this.focusedIndex = -1;
377
+ this.removeClickOutside();
378
+ this.$emit('close');
379
+ }
380
+ }
381
+ },
382
+ beforeDestroy() {
383
+ this.removeClickOutside();
384
+ },
385
+ methods: {
386
+ getOptionKey(option, index) {
387
+ if (typeof option === 'object' && option !== null) {
388
+ return option[this.optionKey] || index;
389
+ }
390
+ return option || index;
391
+ },
392
+ getOptionValue(option) {
393
+ if (typeof option === 'object' && option !== null) {
394
+ return option[this.optionKey];
395
+ }
396
+ return option;
397
+ },
398
+ getOptionLabel(option) {
399
+ if (typeof option === 'object' && option !== null) {
400
+ return option[this.optionLabel] || '';
401
+ }
402
+ return String(option);
403
+ },
404
+ isDisabled(option) {
405
+ if (typeof option === 'object' && option !== null) {
406
+ return option[this.optionDisabled] || false;
407
+ }
408
+ return false;
409
+ },
410
+ isSelected(option) {
411
+ const optionValue = this.getOptionValue(option);
412
+
413
+ if (this.multiple && Array.isArray(this.value)) {
414
+ return this.value.some(
415
+ (val) => this.getOptionValue(val) === optionValue
416
+ );
417
+ }
418
+
419
+ return this.getOptionValue(this.value) === optionValue;
420
+ },
421
+ getOptionClasses(option, index) {
422
+ const baseClasses = 'flex items-center gap-2 px-3 py-2 cursor-pointer';
423
+ const transitionClasses =
424
+ 'transition-colors duration-[var(--transition-duration-fast)] ease-[var(--transition-easing-standard)]';
425
+ const hoverClasses = 'hover:bg-neutral-50';
426
+ const focusedClasses =
427
+ this.focusedIndex === index ? 'bg-neutral-100' : '';
428
+ const selectedClasses = this.isSelected(option)
429
+ ? 'bg-primary-50 text-primary-700'
430
+ : '';
431
+ const disabledClasses = this.isDisabled(option)
432
+ ? 'opacity-50 cursor-not-allowed'
433
+ : '';
434
+
435
+ return [
436
+ baseClasses,
437
+ transitionClasses,
438
+ !this.isDisabled(option) && hoverClasses,
439
+ focusedClasses,
440
+ selectedClasses,
441
+ disabledClasses
442
+ ]
443
+ .filter(Boolean)
444
+ .join(' ');
445
+ },
446
+ toggleDropdown() {
447
+ if (this.disabled) return;
448
+ this.isOpen = !this.isOpen;
449
+ },
450
+ openDropdown() {
451
+ if (this.disabled) return;
452
+ this.isOpen = true;
453
+ },
454
+ closeDropdown() {
455
+ this.isOpen = false;
456
+ },
457
+ handleOptionClick(option) {
458
+ if (this.isDisabled(option)) return;
459
+
460
+ const optionValue = this.getOptionValue(option);
461
+
462
+ if (this.multiple) {
463
+ let newValue = Array.isArray(this.value) ? [...this.value] : [];
464
+ const index = newValue.findIndex(
465
+ (val) => this.getOptionValue(val) === optionValue
466
+ );
467
+
468
+ if (index > -1) {
469
+ newValue.splice(index, 1);
470
+ } else {
471
+ newValue.push(option);
472
+ }
473
+
474
+ this.$emit('input', newValue);
475
+ this.$emit('change', newValue);
476
+ } else {
477
+ this.$emit('input', option);
478
+ this.$emit('change', option);
479
+ this.closeDropdown();
480
+ }
481
+ },
482
+ handleKeyboardNavigation(direction) {
483
+ const maxIndex = this.filteredOptions.length - 1;
484
+
485
+ if (direction === 'down') {
486
+ this.focusedIndex =
487
+ this.focusedIndex < maxIndex ? this.focusedIndex + 1 : 0;
488
+ } else if (direction === 'up') {
489
+ this.focusedIndex =
490
+ this.focusedIndex > 0 ? this.focusedIndex - 1 : maxIndex;
491
+ }
492
+
493
+ this.scrollToFocusedOption();
494
+ },
495
+ handleEnterKey() {
496
+ if (
497
+ this.focusedIndex >= 0 &&
498
+ this.focusedIndex < this.filteredOptions.length
499
+ ) {
500
+ const option = this.filteredOptions[this.focusedIndex];
501
+ this.handleOptionClick(option);
502
+ }
503
+ },
504
+ scrollToFocusedOption() {
505
+ this.$nextTick(() => {
506
+ const optionsList = this.$refs.optionsList;
507
+ // Dynamic refs in v-for return an array, so we access [0]
508
+ const focusedOption = this.$refs[`option-${this.focusedIndex}`];
509
+
510
+ if (optionsList && focusedOption && focusedOption[0]) {
511
+ const optionElement = focusedOption[0];
512
+ const optionTop = optionElement.offsetTop;
513
+ const optionHeight = optionElement.offsetHeight;
514
+ const listScrollTop = optionsList.scrollTop;
515
+ const listHeight = optionsList.clientHeight;
516
+
517
+ if (optionTop < listScrollTop) {
518
+ optionsList.scrollTop = optionTop;
519
+ } else if (optionTop + optionHeight > listScrollTop + listHeight) {
520
+ optionsList.scrollTop = optionTop + optionHeight - listHeight;
521
+ }
522
+ }
523
+ });
524
+ },
525
+ setupClickOutside() {
526
+ this.clickOutsideHandler = (event) => {
527
+ const dropdown = this.$refs.dropdown;
528
+ const trigger = this.$refs.trigger;
529
+ const target = event.target;
530
+
531
+ if (
532
+ target instanceof Node &&
533
+ dropdown &&
534
+ !dropdown.contains(target) &&
535
+ trigger &&
536
+ !trigger.contains(target)
537
+ ) {
538
+ this.closeDropdown();
539
+ }
540
+ };
541
+
542
+ document.addEventListener('click', this.clickOutsideHandler);
543
+ },
544
+ removeClickOutside() {
545
+ if (this.clickOutsideHandler) {
546
+ document.removeEventListener('click', this.clickOutsideHandler);
547
+ }
548
+ }
549
+ }
550
+ };
551
+ </script>
@@ -0,0 +1,144 @@
1
+ import FStatCard from './FStatCard.vue';
2
+
3
+ export default {
4
+ title: 'Molecules/FStatCard',
5
+ component: FStatCard,
6
+ tags: ['autodocs'],
7
+ argTypes: {
8
+ icon: {
9
+ control: 'text',
10
+ description: 'Icône à afficher'
11
+ },
12
+ label: {
13
+ control: 'text',
14
+ description: 'Label/titre de la statistique'
15
+ },
16
+ value: {
17
+ control: 'text',
18
+ description: 'Valeur à afficher'
19
+ },
20
+ variant: {
21
+ control: { type: 'select' },
22
+ options: ['primary', 'success', 'danger', 'info'],
23
+ description: 'Variante de couleur'
24
+ },
25
+ layout: {
26
+ control: { type: 'select' },
27
+ options: ['horizontal', 'vertical'],
28
+ description: 'Disposition'
29
+ },
30
+ bordered: {
31
+ control: 'boolean',
32
+ description: 'Afficher une bordure'
33
+ }
34
+ }
35
+ };
36
+
37
+ const Template = (args, { argTypes }) => ({
38
+ components: { FStatCard },
39
+ props: Object.keys(argTypes),
40
+ template: '<FStatCard v-bind="$props" />'
41
+ });
42
+
43
+ export const Default = Template.bind({});
44
+ Default.args = {
45
+ icon: 'user',
46
+ label: 'Utilisateurs',
47
+ value: '1,234'
48
+ };
49
+
50
+ export const Primary = Template.bind({});
51
+ Primary.args = {
52
+ icon: 'user',
53
+ label: 'Total utilisateurs',
54
+ value: '12,456',
55
+ variant: 'primary'
56
+ };
57
+
58
+ export const Success = Template.bind({});
59
+ Success.args = {
60
+ icon: 'check',
61
+ label: 'Tâches complétées',
62
+ value: '89%',
63
+ variant: 'success'
64
+ };
65
+
66
+ export const Danger = Template.bind({});
67
+ Danger.args = {
68
+ icon: 'warning',
69
+ label: 'Erreurs détectées',
70
+ value: '23',
71
+ variant: 'danger'
72
+ };
73
+
74
+ export const Vertical = Template.bind({});
75
+ Vertical.args = {
76
+ icon: 'star',
77
+ label: 'Note moyenne',
78
+ value: '4.8/5',
79
+ layout: 'vertical'
80
+ };
81
+
82
+ export const NoBorder = Template.bind({});
83
+ NoBorder.args = {
84
+ icon: 'mail',
85
+ label: 'Messages',
86
+ value: '456',
87
+ bordered: false
88
+ };
89
+
90
+ export const AllVariants = () => ({
91
+ components: { FStatCard },
92
+ template: `
93
+ <div class="grid grid-cols-4 gap-4">
94
+ <FStatCard icon="user" label="Utilisateurs" value="1,234" variant="primary" />
95
+ <FStatCard icon="check" label="Complétés" value="89%" variant="success" />
96
+ <FStatCard icon="warning" label="Alertes" value="12" variant="danger" />
97
+ <FStatCard icon="info" label="Informations" value="45" variant="info" />
98
+ </div>
99
+ `
100
+ });
101
+
102
+ export const VerticalLayout = () => ({
103
+ components: { FStatCard },
104
+ template: `
105
+ <div class="grid grid-cols-4 gap-4">
106
+ <FStatCard icon="user" label="Utilisateurs" value="1,234" layout="vertical" />
107
+ <FStatCard icon="document" label="Documents" value="567" layout="vertical" />
108
+ <FStatCard icon="calendar" label="Événements" value="89" layout="vertical" />
109
+ <FStatCard icon="mail" label="Messages" value="2,345" layout="vertical" />
110
+ </div>
111
+ `
112
+ });
113
+
114
+ export const Dashboard = () => ({
115
+ components: { FStatCard },
116
+ template: `
117
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
118
+ <FStatCard
119
+ icon="user"
120
+ label="Nouveaux utilisateurs"
121
+ value="+156"
122
+ variant="primary"
123
+ />
124
+ <FStatCard
125
+ icon="document"
126
+ label="Documents créés"
127
+ value="1,234"
128
+ variant="info"
129
+ />
130
+ <FStatCard
131
+ icon="check"
132
+ label="Taux de complétion"
133
+ value="94%"
134
+ variant="success"
135
+ />
136
+ <FStatCard
137
+ icon="warning"
138
+ label="Problèmes"
139
+ value="7"
140
+ variant="danger"
141
+ />
142
+ </div>
143
+ `
144
+ });