@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,808 @@
1
+ <template>
2
+ <div :class="containerClasses">
3
+ <!-- Toolbar section: Search and Actions -->
4
+ <div v-if="showToolbar" :class="toolbarClasses">
5
+ <div class="flex-1">
6
+ <f-search-bar
7
+ v-if="searchable"
8
+ v-model="searchQuery"
9
+ :placeholder="searchPlaceholder"
10
+ :size="size"
11
+ :disabled="loading"
12
+ @search="handleSearch"
13
+ />
14
+ </div>
15
+ <div v-if="$slots.actions" class="flex-shrink-0">
16
+ <slot name="actions" :selected-items="selectedItems" />
17
+ </div>
18
+ </div>
19
+
20
+ <!-- Table wrapper -->
21
+ <div :class="tableWrapperClasses">
22
+ <!-- Loading overlay -->
23
+ <div v-if="loading" :class="loadingOverlayClasses">
24
+ <f-loader size="large" />
25
+ </div>
26
+
27
+ <!-- Table -->
28
+ <table v-if="processedData.length > 0 || loading" :class="tableClasses">
29
+ <thead>
30
+ <tr>
31
+ <!-- Selection checkbox column -->
32
+ <th v-if="selectable" :class="headerCellClasses">
33
+ <f-checkbox :checked="isAllSelected" @change="handleSelectAll" />
34
+ </th>
35
+ <!-- Data columns -->
36
+ <th
37
+ v-for="column in columns"
38
+ :key="column.key"
39
+ :class="getHeaderCellClasses(column)"
40
+ :aria-sort="getAriaSort(column.key, column.sortable)"
41
+ @click="column.sortable !== false && handleSort(column.key)"
42
+ >
43
+ <div class="flex items-center gap-1">
44
+ <span>{{ column.label }}</span>
45
+ <f-icon
46
+ v-if="column.sortable !== false"
47
+ :name="getSortIcon(column.key)"
48
+ size="sm"
49
+ :class="getSortIconClasses(column.key)"
50
+ />
51
+ </div>
52
+ </th>
53
+ </tr>
54
+ </thead>
55
+ <tbody v-if="!virtual">
56
+ <tr
57
+ v-for="(row, rowIndex) in paginatedData"
58
+ :key="getRowKey(row, rowIndex)"
59
+ :class="getRowClasses(row)"
60
+ @click="handleRowClick(row)"
61
+ >
62
+ <!-- Selection checkbox -->
63
+ <td v-if="selectable" :class="cellClasses" data-label="">
64
+ <f-checkbox
65
+ :checked="isRowSelected(row)"
66
+ @change="handleRowSelect(row, $event)"
67
+ @click.stop
68
+ />
69
+ </td>
70
+ <!-- Data cells -->
71
+ <td
72
+ v-for="column in columns"
73
+ :key="column.key"
74
+ :class="getCellClasses(column)"
75
+ :data-label="column.label"
76
+ >
77
+ <slot
78
+ :name="'cell-' + column.key"
79
+ :value="getCellValue(row, column.key)"
80
+ :row="row"
81
+ :column="column"
82
+ >
83
+ {{ getCellValue(row, column.key) }}
84
+ </slot>
85
+ </td>
86
+ </tr>
87
+ </tbody>
88
+ </table>
89
+
90
+ <!-- Virtual scrolling table body -->
91
+ <div
92
+ v-if="virtual && (processedData.length > 0 || loading)"
93
+ class="virtual-table-body"
94
+ >
95
+ <RecycleScroller
96
+ :items="paginatedData"
97
+ :item-size="computedVirtualItemHeight"
98
+ :key-field="rowKey"
99
+ :buffer="200"
100
+ class="scroller"
101
+ :style="{ height: virtualHeight + 'px' }"
102
+ >
103
+ <template #default="{ item: row }">
104
+ <div
105
+ :class="['virtual-row', getRowClasses(row)]"
106
+ @click="handleRowClick(row)"
107
+ >
108
+ <!-- Selection checkbox -->
109
+ <div v-if="selectable" :class="['virtual-cell', cellClasses]">
110
+ <f-checkbox
111
+ :checked="isRowSelected(row)"
112
+ @change="handleRowSelect(row, $event)"
113
+ @click.stop
114
+ />
115
+ </div>
116
+ <!-- Data cells -->
117
+ <div
118
+ v-for="column in columns"
119
+ :key="column.key"
120
+ :class="['virtual-cell', getCellClasses(column)]"
121
+ >
122
+ <slot
123
+ :name="'cell-' + column.key"
124
+ :value="getCellValue(row, column.key)"
125
+ :row="row"
126
+ :column="column"
127
+ >
128
+ {{ getCellValue(row, column.key) }}
129
+ </slot>
130
+ </div>
131
+ </div>
132
+ </template>
133
+ </RecycleScroller>
134
+ </div>
135
+
136
+ <!-- Empty state -->
137
+ <f-empty-state
138
+ v-if="!loading && processedData.length === 0"
139
+ :icon="emptyIcon"
140
+ :title="emptyTitle"
141
+ :description="emptyDescription"
142
+ :action-label="emptyActionLabel"
143
+ @action="$emit('empty-action')"
144
+ />
145
+ </div>
146
+
147
+ <!-- Footer section: Info and Pagination -->
148
+ <div v-if="showFooter" :class="footerClasses">
149
+ <div :class="infoClasses">
150
+ <span v-if="selectable && selectedItems.length > 0">
151
+ {{ selectedItems.length }} élément(s) sélectionné(s) sur
152
+ {{ totalItems }}
153
+ </span>
154
+ <span v-else>
155
+ {{ paginationInfo }}
156
+ </span>
157
+ </div>
158
+ <f-pagination
159
+ v-if="effectivePaginated && totalPages > 1"
160
+ v-model="internalPage"
161
+ :total-pages="totalPages"
162
+ :size="size"
163
+ :show-labels="false"
164
+ @change="handlePageChange"
165
+ />
166
+ </div>
167
+ </div>
168
+ </template>
169
+
170
+ <script>
171
+ import FSearchBar from '../../molecules/FSearchBar/FSearchBar.vue';
172
+ import FPagination from '../../molecules/FPagination/FPagination.vue';
173
+ import FEmptyState from '../../molecules/FEmptyState/FEmptyState.vue';
174
+ import FCheckbox from '../../atoms/FCheckbox/FCheckbox.vue';
175
+ import FIcon from '../../atoms/FIcon/FIcon.vue';
176
+ import FLoader from '../../atoms/FLoader/FLoader.vue';
177
+ import { RecycleScroller } from 'vue-virtual-scroller';
178
+ import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
179
+
180
+ export default {
181
+ name: 'FDataTable',
182
+ components: {
183
+ FSearchBar,
184
+ FPagination,
185
+ FEmptyState,
186
+ FCheckbox,
187
+ FIcon,
188
+ FLoader,
189
+ RecycleScroller
190
+ },
191
+ props: {
192
+ /**
193
+ * Array of data objects to display
194
+ */
195
+ data: {
196
+ type: Array,
197
+ default: () => []
198
+ },
199
+ /**
200
+ * Column definitions
201
+ * Each column: { key: string, label: string, sortable?: boolean, align?: 'left'|'center'|'right' }
202
+ */
203
+ columns: {
204
+ type: Array,
205
+ required: true,
206
+ validator: (columns) => columns.every((col) => col.key && col.label)
207
+ },
208
+ /**
209
+ * Unique key property in data objects
210
+ */
211
+ rowKey: {
212
+ type: String,
213
+ default: 'id'
214
+ },
215
+ /**
216
+ * Enable row selection with checkboxes
217
+ */
218
+ selectable: {
219
+ type: Boolean,
220
+ default: false
221
+ },
222
+ /**
223
+ * Selected row keys (v-model:selected)
224
+ */
225
+ selected: {
226
+ type: Array,
227
+ default: () => []
228
+ },
229
+ /**
230
+ * Enable search functionality
231
+ */
232
+ searchable: {
233
+ type: Boolean,
234
+ default: false
235
+ },
236
+ /**
237
+ * Search input placeholder
238
+ */
239
+ searchPlaceholder: {
240
+ type: String,
241
+ default: 'Rechercher...'
242
+ },
243
+ /**
244
+ * Enable pagination
245
+ */
246
+ paginated: {
247
+ type: Boolean,
248
+ default: true
249
+ },
250
+ /**
251
+ * Number of items per page
252
+ */
253
+ perPage: {
254
+ type: Number,
255
+ default: 10
256
+ },
257
+ /**
258
+ * Current page (v-model:page)
259
+ */
260
+ page: {
261
+ type: Number,
262
+ default: 1
263
+ },
264
+ /**
265
+ * Total items count for server-side pagination
266
+ */
267
+ totalItems: {
268
+ type: Number,
269
+ default: null
270
+ },
271
+ /**
272
+ * Server mode - data fetching is handled externally
273
+ */
274
+ serverMode: {
275
+ type: Boolean,
276
+ default: false
277
+ },
278
+ /**
279
+ * Loading state
280
+ */
281
+ loading: {
282
+ type: Boolean,
283
+ default: false
284
+ },
285
+ /**
286
+ * Default sort column key
287
+ */
288
+ defaultSortKey: {
289
+ type: String,
290
+ default: null
291
+ },
292
+ /**
293
+ * Default sort direction
294
+ */
295
+ defaultSortDirection: {
296
+ type: String,
297
+ default: 'asc',
298
+ validator: (value) => ['asc', 'desc'].includes(value)
299
+ },
300
+ /**
301
+ * Component size
302
+ */
303
+ size: {
304
+ type: String,
305
+ default: 'medium',
306
+ validator: (value) => ['small', 'medium', 'large'].includes(value)
307
+ },
308
+ /**
309
+ * Empty state icon
310
+ */
311
+ emptyIcon: {
312
+ type: String,
313
+ default: 'folder'
314
+ },
315
+ /**
316
+ * Empty state title
317
+ */
318
+ emptyTitle: {
319
+ type: String,
320
+ default: 'Aucune donnée'
321
+ },
322
+ /**
323
+ * Empty state description
324
+ */
325
+ emptyDescription: {
326
+ type: String,
327
+ default: "Il n'y a aucun élément à afficher."
328
+ },
329
+ /**
330
+ * Empty state action button label
331
+ */
332
+ emptyActionLabel: {
333
+ type: String,
334
+ default: ''
335
+ },
336
+ /**
337
+ * Striped row style
338
+ */
339
+ striped: {
340
+ type: Boolean,
341
+ default: false
342
+ },
343
+ /**
344
+ * Hoverable rows
345
+ */
346
+ hoverable: {
347
+ type: Boolean,
348
+ default: true
349
+ },
350
+ /**
351
+ * Bordered table
352
+ */
353
+ bordered: {
354
+ type: Boolean,
355
+ default: false
356
+ },
357
+ /**
358
+ * Enable virtualization for large datasets (improves performance with 1000+ rows)
359
+ * When enabled, only visible rows are rendered. Pagination is automatically disabled.
360
+ */
361
+ virtual: {
362
+ type: Boolean,
363
+ default: false
364
+ },
365
+ /**
366
+ * Height of each virtualized row in pixels
367
+ * Used only when virtual is enabled
368
+ */
369
+ virtualItemHeight: {
370
+ type: Number,
371
+ default: null
372
+ },
373
+ /**
374
+ * Height of the virtual scroller container in pixels
375
+ * Used only when virtual is enabled
376
+ */
377
+ virtualHeight: {
378
+ type: Number,
379
+ default: 500
380
+ }
381
+ },
382
+ data() {
383
+ return {
384
+ searchQuery: '',
385
+ sortKey: this.defaultSortKey,
386
+ sortDirection: this.defaultSortDirection,
387
+ internalPage: this.page,
388
+ selectedKeys: [...this.selected]
389
+ };
390
+ },
391
+ computed: {
392
+ containerClasses() {
393
+ return 'flex flex-col gap-4 bg-white rounded-lg';
394
+ },
395
+ toolbarClasses() {
396
+ return 'flex items-center gap-4 flex-wrap';
397
+ },
398
+ tableWrapperClasses() {
399
+ const baseClasses = 'relative overflow-x-auto';
400
+ const borderClasses = this.bordered
401
+ ? 'border border-neutral-200 rounded-lg'
402
+ : '';
403
+ return [baseClasses, borderClasses].filter(Boolean).join(' ');
404
+ },
405
+ loadingOverlayClasses() {
406
+ return 'absolute inset-0 bg-white/80 flex items-center justify-center z-10';
407
+ },
408
+ tableClasses() {
409
+ return 'w-full text-left';
410
+ },
411
+ headerCellClasses() {
412
+ const sizeClasses = {
413
+ small: 'px-3 py-2 text-xs',
414
+ medium: 'px-4 py-3 text-sm',
415
+ large: 'px-6 py-4 text-base'
416
+ };
417
+ return [
418
+ 'font-semibold text-neutral-700 bg-neutral-50 border-b border-neutral-200',
419
+ sizeClasses[this.size]
420
+ ].join(' ');
421
+ },
422
+ cellClasses() {
423
+ const sizeClasses = {
424
+ small: 'px-3 py-2 text-xs',
425
+ medium: 'px-4 py-3 text-sm',
426
+ large: 'px-6 py-4 text-base'
427
+ };
428
+ return [
429
+ 'text-neutral-600 border-b border-neutral-100',
430
+ sizeClasses[this.size]
431
+ ].join(' ');
432
+ },
433
+ footerClasses() {
434
+ return 'flex items-center justify-between gap-4 flex-wrap';
435
+ },
436
+ infoClasses() {
437
+ const sizeClasses = {
438
+ small: 'text-xs',
439
+ medium: 'text-sm',
440
+ large: 'text-base'
441
+ };
442
+ return ['text-neutral-500', sizeClasses[this.size]].join(' ');
443
+ },
444
+ showToolbar() {
445
+ return this.searchable || this.$slots.actions;
446
+ },
447
+ showFooter() {
448
+ return this.effectivePaginated || this.selectable;
449
+ },
450
+ // Filter data based on search query (client-side only)
451
+ filteredData() {
452
+ if (this.serverMode || !this.searchQuery) {
453
+ return this.data;
454
+ }
455
+ const query = this.searchQuery.toLowerCase();
456
+ return this.data.filter((row) => {
457
+ return this.columns.some((column) => {
458
+ const value = this.getCellValue(row, column.key);
459
+ return String(value).toLowerCase().includes(query);
460
+ });
461
+ });
462
+ },
463
+ // Sort filtered data (client-side only)
464
+ sortedData() {
465
+ if (this.serverMode || !this.sortKey) {
466
+ return this.filteredData;
467
+ }
468
+ return [...this.filteredData].sort((a, b) => {
469
+ const aValue = this.getCellValue(a, this.sortKey);
470
+ const bValue = this.getCellValue(b, this.sortKey);
471
+
472
+ let comparison = 0;
473
+ if (aValue === null || aValue === undefined) comparison = 1;
474
+ else if (bValue === null || bValue === undefined) comparison = -1;
475
+ else if (typeof aValue === 'string') {
476
+ comparison = aValue.localeCompare(bValue);
477
+ } else {
478
+ comparison = aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
479
+ }
480
+
481
+ return this.sortDirection === 'desc' ? -comparison : comparison;
482
+ });
483
+ },
484
+ // Processed data after filtering and sorting
485
+ processedData() {
486
+ return this.sortedData;
487
+ },
488
+ // Calculate total items for pagination
489
+ computedTotalItems() {
490
+ if (this.serverMode && this.totalItems !== null) {
491
+ return this.totalItems;
492
+ }
493
+ return this.processedData.length;
494
+ },
495
+ // Total pages
496
+ totalPages() {
497
+ if (!this.effectivePaginated) return 1;
498
+ return Math.max(1, Math.ceil(this.computedTotalItems / this.perPage));
499
+ },
500
+ // Data for current page (client-side pagination only)
501
+ paginatedData() {
502
+ if (this.serverMode || !this.effectivePaginated) {
503
+ return this.processedData;
504
+ }
505
+ const start = (this.internalPage - 1) * this.perPage;
506
+ const end = start + this.perPage;
507
+ return this.processedData.slice(start, end);
508
+ },
509
+ // Pagination info text
510
+ paginationInfo() {
511
+ if (!this.effectivePaginated) {
512
+ return `${this.computedTotalItems} élément(s)`;
513
+ }
514
+ const start = Math.min(
515
+ (this.internalPage - 1) * this.perPage + 1,
516
+ this.computedTotalItems
517
+ );
518
+ const end = Math.min(
519
+ this.internalPage * this.perPage,
520
+ this.computedTotalItems
521
+ );
522
+ return `${start} - ${end} sur ${this.computedTotalItems}`;
523
+ },
524
+ // Set for efficient key lookups
525
+ selectedKeysSet() {
526
+ return new Set(this.selectedKeys);
527
+ },
528
+ // Selection state
529
+ selectedItems() {
530
+ return this.data.filter((row) =>
531
+ this.selectedKeysSet.has(this.getRowKey(row))
532
+ );
533
+ },
534
+ isAllSelected() {
535
+ if (this.paginatedData.length === 0) return false;
536
+ return this.paginatedData.every((row) => this.isRowSelected(row));
537
+ },
538
+ // Calculate virtual item height based on size
539
+ computedVirtualItemHeight() {
540
+ if (this.virtualItemHeight !== null) {
541
+ return this.virtualItemHeight;
542
+ }
543
+ // Auto-calculate based on size prop
544
+ const sizeHeights = {
545
+ small: 40,
546
+ medium: 52,
547
+ large: 64
548
+ };
549
+ return sizeHeights[this.size] || sizeHeights.medium;
550
+ },
551
+ // When virtual mode is enabled, don't paginate
552
+ effectivePaginated() {
553
+ return this.virtual ? false : this.paginated;
554
+ }
555
+ },
556
+ watch: {
557
+ page: {
558
+ handler(newVal) {
559
+ this.internalPage = newVal;
560
+ },
561
+ immediate: true
562
+ },
563
+ internalPage(newVal) {
564
+ this.$emit('update:page', newVal);
565
+ },
566
+ selected: {
567
+ handler(newVal) {
568
+ this.selectedKeys = [...newVal];
569
+ },
570
+ deep: true,
571
+ immediate: true
572
+ },
573
+ selectedKeys: {
574
+ handler(newVal) {
575
+ this.$emit('update:selected', newVal);
576
+ },
577
+ deep: true
578
+ },
579
+ searchQuery() {
580
+ // Reset to first page when search changes
581
+ if (!this.serverMode) {
582
+ this.internalPage = 1;
583
+ }
584
+ }
585
+ },
586
+ methods: {
587
+ getCellValue(row, key) {
588
+ // Support nested keys like 'user.name'
589
+ return key.split('.').reduce((obj, k) => obj?.[k], row);
590
+ },
591
+ getRowKey(row, index) {
592
+ return row[this.rowKey] ?? index;
593
+ },
594
+ getHeaderCellClasses(column) {
595
+ const alignClasses = {
596
+ left: 'text-left',
597
+ center: 'text-center',
598
+ right: 'text-right'
599
+ };
600
+ const sortableClasses =
601
+ column.sortable !== false
602
+ ? 'cursor-pointer select-none hover:bg-neutral-100'
603
+ : '';
604
+ return [
605
+ this.headerCellClasses,
606
+ alignClasses[column.align] || 'text-left',
607
+ sortableClasses
608
+ ]
609
+ .filter(Boolean)
610
+ .join(' ');
611
+ },
612
+ getCellClasses(column) {
613
+ const alignClasses = {
614
+ left: 'text-left',
615
+ center: 'text-center',
616
+ right: 'text-right'
617
+ };
618
+ return [this.cellClasses, alignClasses[column.align] || 'text-left'].join(
619
+ ' '
620
+ );
621
+ },
622
+ getRowClasses(row) {
623
+ const baseClasses =
624
+ 'transition-colors duration-[var(--transition-duration-fast)] ease-[var(--transition-easing-standard)]';
625
+ const hoverClasses = this.hoverable ? 'hover:bg-neutral-50' : '';
626
+ const selectedClasses = this.isRowSelected(row) ? 'bg-primary-50' : '';
627
+ const stripedClasses = this.striped ? 'even:bg-neutral-50/50' : '';
628
+ return [baseClasses, hoverClasses, selectedClasses, stripedClasses]
629
+ .filter(Boolean)
630
+ .join(' ');
631
+ },
632
+ getSortIcon(key) {
633
+ if (this.sortKey !== key) return 'chevron-down';
634
+ return this.sortDirection === 'asc' ? 'chevron-up' : 'chevron-down';
635
+ },
636
+ getSortIconClasses(key) {
637
+ const isActive = this.sortKey === key;
638
+ return isActive ? 'text-primary-500' : 'text-neutral-400';
639
+ },
640
+ getAriaSort(key, sortable) {
641
+ // Don't add aria-sort if column is not sortable
642
+ if (sortable === false) {
643
+ return undefined;
644
+ }
645
+ // If this column is currently sorted, indicate direction
646
+ if (this.sortKey === key) {
647
+ return this.sortDirection === 'asc' ? 'ascending' : 'descending';
648
+ }
649
+ // Column is sortable but not currently sorted
650
+ return 'none';
651
+ },
652
+ handleSort(key) {
653
+ if (this.sortKey === key) {
654
+ this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
655
+ } else {
656
+ this.sortKey = key;
657
+ this.sortDirection = 'asc';
658
+ }
659
+ this.$emit('sort', { key: this.sortKey, direction: this.sortDirection });
660
+ },
661
+ handleSearch(query) {
662
+ this.$emit('search', query);
663
+ },
664
+ handlePageChange(page) {
665
+ this.$emit('page-change', page);
666
+ },
667
+ handleRowClick(row) {
668
+ this.$emit('row-click', row);
669
+ },
670
+ isRowSelected(row) {
671
+ return this.selectedKeysSet.has(this.getRowKey(row));
672
+ },
673
+ handleRowSelect(row, checked) {
674
+ const key = this.getRowKey(row);
675
+ if (checked) {
676
+ if (!this.selectedKeysSet.has(key)) {
677
+ this.selectedKeys = [...this.selectedKeys, key];
678
+ }
679
+ } else {
680
+ this.selectedKeys = this.selectedKeys.filter((k) => k !== key);
681
+ }
682
+ this.$emit('select', { row, selected: checked });
683
+ },
684
+ handleSelectAll(checked) {
685
+ if (checked) {
686
+ const currentKeys = this.paginatedData.map((row) =>
687
+ this.getRowKey(row)
688
+ );
689
+ const newKeys = currentKeys.filter(
690
+ (k) => !this.selectedKeys.includes(k)
691
+ );
692
+ this.selectedKeys = [...this.selectedKeys, ...newKeys];
693
+ } else {
694
+ const currentKeys = this.paginatedData.map((row) =>
695
+ this.getRowKey(row)
696
+ );
697
+ this.selectedKeys = this.selectedKeys.filter(
698
+ (k) => !currentKeys.includes(k)
699
+ );
700
+ }
701
+ this.$emit('select-all', checked);
702
+ },
703
+ clearSelection() {
704
+ this.selectedKeys = [];
705
+ }
706
+ }
707
+ };
708
+ </script>
709
+
710
+ <style scoped>
711
+ /* Virtual table styling */
712
+ .virtual-table-body {
713
+ border: 1px solid var(--color-neutral-200, #e5e7eb);
714
+ border-radius: 0.5rem;
715
+ overflow: hidden;
716
+ }
717
+
718
+ .virtual-table-body .scroller {
719
+ width: 100%;
720
+ }
721
+
722
+ .virtual-row {
723
+ display: flex;
724
+ align-items: center;
725
+ border-bottom: 1px solid var(--color-neutral-100, #f3f4f6);
726
+ transition: background-color 0.15s;
727
+ }
728
+
729
+ .virtual-row:last-child {
730
+ border-bottom: none;
731
+ }
732
+
733
+ .virtual-cell {
734
+ flex: 1;
735
+ min-width: 0;
736
+ display: flex;
737
+ align-items: center;
738
+ }
739
+
740
+ /* Mobile Card View - transforms table rows into cards on small screens */
741
+ @media (max-width: 640px) {
742
+ /* Hide table header on mobile */
743
+ table thead {
744
+ display: none;
745
+ }
746
+
747
+ /* Make table body a flex container for cards */
748
+ table tbody {
749
+ display: flex;
750
+ flex-direction: column;
751
+ gap: 0.75rem;
752
+ }
753
+
754
+ /* Transform each row into a card */
755
+ table tbody tr {
756
+ display: flex;
757
+ flex-direction: column;
758
+ background-color: white;
759
+ border: 1px solid var(--color-neutral-200, #e5e7eb);
760
+ border-radius: 0.5rem;
761
+ padding: 0.75rem;
762
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
763
+ }
764
+
765
+ /* Style each cell as a row in the card */
766
+ table tbody tr td {
767
+ display: flex;
768
+ justify-content: space-between;
769
+ align-items: flex-start;
770
+ padding: 0.5rem 0;
771
+ border-bottom: 1px solid var(--color-neutral-100, #f3f4f6);
772
+ text-align: right;
773
+ }
774
+
775
+ /* Remove border from last cell */
776
+ table tbody tr td:last-child {
777
+ border-bottom: none;
778
+ }
779
+
780
+ /* Display column label before cell content */
781
+ table tbody tr td::before {
782
+ content: attr(data-label);
783
+ font-weight: 600;
784
+ color: var(--color-neutral-700, #374151);
785
+ text-align: left;
786
+ flex-shrink: 0;
787
+ margin-right: 1rem;
788
+ }
789
+
790
+ /* Hide empty labels (for checkbox column) */
791
+ table tbody tr td[data-label='']::before {
792
+ display: none;
793
+ }
794
+
795
+ /* Checkbox cell styling */
796
+ table tbody tr td[data-label=''] {
797
+ justify-content: flex-start;
798
+ border-bottom: 1px solid var(--color-neutral-200, #e5e7eb);
799
+ margin-bottom: 0.25rem;
800
+ padding-bottom: 0.75rem;
801
+ }
802
+
803
+ /* Ensure table is full width */
804
+ table {
805
+ width: 100%;
806
+ }
807
+ }
808
+ </style>