@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,361 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { ref, computed, watch, Ref, ComputedRef } from 'vue';
3
+
4
+ /**
5
+ * Configuration options for useDataTableState
6
+ */
7
+ export interface DataTableStateOptions {
8
+ /**
9
+ * Table data
10
+ */
11
+ data: any[];
12
+ /**
13
+ * Column definitions
14
+ */
15
+ columns: Array<{
16
+ key: string;
17
+ label: string;
18
+ sortable?: boolean;
19
+ align?: string;
20
+ }>;
21
+ /**
22
+ * Unique key property in data objects
23
+ */
24
+ rowKey?: string;
25
+ /**
26
+ * Initial page number
27
+ */
28
+ initialPage?: number;
29
+ /**
30
+ * Items per page
31
+ */
32
+ perPage?: number;
33
+ /**
34
+ * Enable pagination
35
+ */
36
+ paginated?: boolean;
37
+ /**
38
+ * Enable virtualization (disables pagination)
39
+ */
40
+ virtual?: boolean;
41
+ /**
42
+ * Default sort key
43
+ */
44
+ defaultSortKey?: string | null;
45
+ /**
46
+ * Default sort direction
47
+ */
48
+ defaultSortDirection?: 'asc' | 'desc';
49
+ /**
50
+ * Server mode (external data fetching)
51
+ */
52
+ serverMode?: boolean;
53
+ /**
54
+ * Total items for server-side pagination
55
+ */
56
+ totalItems?: number | null;
57
+ /**
58
+ * Initially selected row keys
59
+ */
60
+ selected?: any[];
61
+ }
62
+
63
+ /**
64
+ * Return type for useDataTableState
65
+ */
66
+ export interface DataTableState {
67
+ // Search state
68
+ searchQuery: Ref<string>;
69
+
70
+ // Sort state
71
+ sortKey: Ref<string | null>;
72
+ sortDirection: Ref<'asc' | 'desc'>;
73
+
74
+ // Pagination state
75
+ internalPage: Ref<number>;
76
+ totalPages: ComputedRef<number>;
77
+ paginationInfo: ComputedRef<string>;
78
+ effectivePaginated: ComputedRef<boolean>;
79
+
80
+ // Selection state
81
+ selectedKeys: Ref<any[]>;
82
+ selectedKeysSet: ComputedRef<Set<any>>;
83
+ selectedItems: ComputedRef<any[]>;
84
+ isAllSelected: ComputedRef<boolean>;
85
+
86
+ // Data processing
87
+ filteredData: ComputedRef<any[]>;
88
+ sortedData: ComputedRef<any[]>;
89
+ processedData: ComputedRef<any[]>;
90
+ paginatedData: ComputedRef<any[]>;
91
+ computedTotalItems: ComputedRef<number>;
92
+
93
+ // Methods
94
+ getCellValue: (row: any, key: string) => any;
95
+ getRowKey: (row: any, index?: number) => any;
96
+ handleSort: (key: string) => void;
97
+ isRowSelected: (row: any) => boolean;
98
+ handleRowSelect: (row: any, checked: boolean) => void;
99
+ handleSelectAll: (checked: boolean) => void;
100
+ clearSelection: () => void;
101
+ }
102
+
103
+ /**
104
+ * Composable for managing data table state and logic
105
+ *
106
+ * Handles filtering, sorting, pagination, and row selection for data tables.
107
+ *
108
+ * @param options - Configuration options for the data table state
109
+ * @param emit - Emit function from the component setup
110
+ * @returns Object containing reactive state and methods for table operations
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const tableState = useDataTableState({
115
+ * data: myData,
116
+ * columns: myColumns,
117
+ * perPage: 10
118
+ * }, emit);
119
+ * ```
120
+ */
121
+ export function useDataTableState(
122
+ options: DataTableStateOptions,
123
+ emit: (event: string, ...args: any[]) => void
124
+ ): DataTableState {
125
+ const {
126
+ data,
127
+ columns,
128
+ rowKey = 'id',
129
+ initialPage = 1,
130
+ perPage = 10,
131
+ paginated = true,
132
+ virtual = false,
133
+ defaultSortKey = null,
134
+ defaultSortDirection = 'asc',
135
+ serverMode = false,
136
+ totalItems = null,
137
+ selected = []
138
+ } = options;
139
+
140
+ // Reactive state
141
+ const searchQuery = ref('');
142
+ const sortKey = ref<string | null>(defaultSortKey);
143
+ const sortDirection = ref<'asc' | 'desc'>(defaultSortDirection);
144
+ const internalPage = ref(initialPage);
145
+ const selectedKeys = ref([...selected]);
146
+
147
+ // Helper: Get cell value with support for nested keys
148
+ const getCellValue = (row: any, key: string): any => {
149
+ return key.split('.').reduce((obj, k) => obj?.[k], row);
150
+ };
151
+
152
+ // Helper: Get row key
153
+ const getRowKey = (row: any, index?: number): any => {
154
+ return row[rowKey] ?? index;
155
+ };
156
+
157
+ // Computed: Filtered data (client-side only)
158
+ const filteredData = computed(() => {
159
+ if (serverMode || !searchQuery.value) {
160
+ return data;
161
+ }
162
+ const query = searchQuery.value.toLowerCase();
163
+ return data.filter((row) => {
164
+ return columns.some((column) => {
165
+ const value = getCellValue(row, column.key);
166
+ return String(value).toLowerCase().includes(query);
167
+ });
168
+ });
169
+ });
170
+
171
+ // Computed: Sorted data (client-side only)
172
+ const sortedData = computed(() => {
173
+ if (serverMode || !sortKey.value) {
174
+ return filteredData.value;
175
+ }
176
+ return [...filteredData.value].sort((a, b) => {
177
+ const aValue = getCellValue(a, sortKey.value as string);
178
+ const bValue = getCellValue(b, sortKey.value as string);
179
+
180
+ let comparison = 0;
181
+ if (aValue === null || aValue === undefined) comparison = 1;
182
+ else if (bValue === null || bValue === undefined) comparison = -1;
183
+ else if (typeof aValue === 'string') {
184
+ comparison = aValue.localeCompare(bValue);
185
+ } else {
186
+ comparison = aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
187
+ }
188
+
189
+ return sortDirection.value === 'desc' ? -comparison : comparison;
190
+ });
191
+ });
192
+
193
+ // Computed: Processed data after filtering and sorting
194
+ const processedData = computed(() => sortedData.value);
195
+
196
+ // Computed: Total items for pagination
197
+ const computedTotalItems = computed(() => {
198
+ if (serverMode && totalItems !== null) {
199
+ return totalItems;
200
+ }
201
+ return processedData.value.length;
202
+ });
203
+
204
+ // Computed: Effective pagination (disabled when virtual mode is enabled)
205
+ const effectivePaginated = computed(() => (virtual ? false : paginated));
206
+
207
+ // Computed: Total pages
208
+ const totalPages = computed(() => {
209
+ if (!effectivePaginated.value) return 1;
210
+ return Math.max(1, Math.ceil(computedTotalItems.value / perPage));
211
+ });
212
+
213
+ // Computed: Paginated data (client-side only)
214
+ const paginatedData = computed(() => {
215
+ if (serverMode || !effectivePaginated.value) {
216
+ return processedData.value;
217
+ }
218
+ const start = (internalPage.value - 1) * perPage;
219
+ const end = start + perPage;
220
+ return processedData.value.slice(start, end);
221
+ });
222
+
223
+ // Computed: Pagination info text
224
+ const paginationInfo = computed(() => {
225
+ if (!effectivePaginated.value) {
226
+ return `${computedTotalItems.value} élément(s)`;
227
+ }
228
+ const start = Math.min(
229
+ (internalPage.value - 1) * perPage + 1,
230
+ computedTotalItems.value
231
+ );
232
+ const end = Math.min(
233
+ internalPage.value * perPage,
234
+ computedTotalItems.value
235
+ );
236
+ return `${start} - ${end} sur ${computedTotalItems.value}`;
237
+ });
238
+
239
+ // Computed: Selected keys as a Set for efficient lookups
240
+ const selectedKeysSet = computed(() => new Set(selectedKeys.value));
241
+
242
+ // Computed: Selected items
243
+ const selectedItems = computed(() => {
244
+ return data.filter((row) => selectedKeysSet.value.has(getRowKey(row)));
245
+ });
246
+
247
+ // Computed: Is all selected
248
+ const isAllSelected = computed(() => {
249
+ if (paginatedData.value.length === 0) return false;
250
+ return paginatedData.value.every((row) => isRowSelected(row));
251
+ });
252
+
253
+ // Method: Check if row is selected
254
+ const isRowSelected = (row: any): boolean => {
255
+ return selectedKeysSet.value.has(getRowKey(row));
256
+ };
257
+
258
+ // Method: Handle sort
259
+ const handleSort = (key: string): void => {
260
+ if (sortKey.value === key) {
261
+ sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc';
262
+ } else {
263
+ sortKey.value = key;
264
+ sortDirection.value = 'asc';
265
+ }
266
+ emit('sort', { key: sortKey.value, direction: sortDirection.value });
267
+ };
268
+
269
+ // Method: Handle row selection
270
+ const handleRowSelect = (row: any, checked: boolean): void => {
271
+ const key = getRowKey(row);
272
+ if (checked) {
273
+ if (!selectedKeysSet.value.has(key)) {
274
+ selectedKeys.value = [...selectedKeys.value, key];
275
+ }
276
+ } else {
277
+ selectedKeys.value = selectedKeys.value.filter((k) => k !== key);
278
+ }
279
+ emit('select', { row, selected: checked });
280
+ };
281
+
282
+ // Method: Handle select all
283
+ const handleSelectAll = (checked: boolean): void => {
284
+ if (checked) {
285
+ const currentKeys = paginatedData.value.map((row) => getRowKey(row));
286
+ const newKeys = currentKeys.filter(
287
+ (k) => !selectedKeys.value.includes(k)
288
+ );
289
+ selectedKeys.value = [...selectedKeys.value, ...newKeys];
290
+ } else {
291
+ const currentKeys = paginatedData.value.map((row) => getRowKey(row));
292
+ selectedKeys.value = selectedKeys.value.filter(
293
+ (k) => !currentKeys.includes(k)
294
+ );
295
+ }
296
+ emit('select-all', checked);
297
+ };
298
+
299
+ // Method: Clear selection
300
+ const clearSelection = (): void => {
301
+ selectedKeys.value = [];
302
+ };
303
+
304
+ // Watch: Reset to first page when search changes (client-side only)
305
+ watch(searchQuery, () => {
306
+ if (!serverMode) {
307
+ internalPage.value = 1;
308
+ }
309
+ });
310
+
311
+ // Watch: Emit page changes
312
+ watch(internalPage, (newVal) => {
313
+ emit('update:page', newVal);
314
+ });
315
+
316
+ // Watch: Emit selection changes
317
+ watch(
318
+ selectedKeys,
319
+ (newVal) => {
320
+ emit('update:selected', newVal);
321
+ },
322
+ { deep: true }
323
+ );
324
+
325
+ return {
326
+ // Search state
327
+ searchQuery,
328
+
329
+ // Sort state
330
+ sortKey,
331
+ sortDirection,
332
+
333
+ // Pagination state
334
+ internalPage,
335
+ totalPages,
336
+ paginationInfo,
337
+ effectivePaginated,
338
+
339
+ // Selection state
340
+ selectedKeys,
341
+ selectedKeysSet,
342
+ selectedItems,
343
+ isAllSelected,
344
+
345
+ // Data processing
346
+ filteredData,
347
+ sortedData,
348
+ processedData,
349
+ paginatedData,
350
+ computedTotalItems,
351
+
352
+ // Methods
353
+ getCellValue,
354
+ getRowKey,
355
+ handleSort,
356
+ isRowSelected,
357
+ handleRowSelect,
358
+ handleSelectAll,
359
+ clearSelection
360
+ };
361
+ }
@@ -0,0 +1,198 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { useFormValidation } from './useFormValidation';
3
+
4
+ describe('useFormValidation', () => {
5
+ it('initializes with default state', () => {
6
+ const emit = vi.fn();
7
+ const state = useFormValidation({}, emit);
8
+
9
+ expect(state.formData.value).toEqual({});
10
+ expect(state.errors.value).toEqual({});
11
+ expect(state.isValid.value).toBe(true);
12
+ expect(state.isSubmitting.value).toBe(false);
13
+ });
14
+
15
+ it('initializes with initial data', () => {
16
+ const emit = vi.fn();
17
+ const initialData = { name: 'John', email: 'john@test.com' };
18
+ const state = useFormValidation({ initialData }, emit);
19
+
20
+ expect(state.formData.value).toEqual(initialData);
21
+ });
22
+
23
+ describe('setFieldValue', () => {
24
+ it('sets a field value', () => {
25
+ const emit = vi.fn();
26
+ const state = useFormValidation({}, emit);
27
+
28
+ state.setFieldValue('name', 'Alice');
29
+ expect(state.formData.value.name).toBe('Alice');
30
+ });
31
+
32
+ it('clears field error when value is updated', () => {
33
+ const emit = vi.fn();
34
+ const state = useFormValidation({}, emit);
35
+
36
+ state.setFieldError('name', 'Required');
37
+ expect(state.errors.value.name).toBe('Required');
38
+
39
+ state.setFieldValue('name', 'Alice');
40
+ expect(state.errors.value.name).toBeUndefined();
41
+ });
42
+ });
43
+
44
+ describe('error handling', () => {
45
+ it('sets a field error', () => {
46
+ const emit = vi.fn();
47
+ const state = useFormValidation({}, emit);
48
+
49
+ state.setFieldError('email', 'Invalid email');
50
+ expect(state.errors.value.email).toBe('Invalid email');
51
+ expect(state.isValid.value).toBe(false);
52
+ });
53
+
54
+ it('clears a specific field error', () => {
55
+ const emit = vi.fn();
56
+ const state = useFormValidation({}, emit);
57
+
58
+ state.setFieldError('email', 'Invalid email');
59
+ state.setFieldError('name', 'Required');
60
+
61
+ state.clearFieldError('email');
62
+ expect(state.errors.value.email).toBeUndefined();
63
+ expect(state.errors.value.name).toBe('Required');
64
+ expect(state.isValid.value).toBe(false);
65
+ });
66
+
67
+ it('updates isValid when all errors are cleared individually', () => {
68
+ const emit = vi.fn();
69
+ const state = useFormValidation({}, emit);
70
+
71
+ state.setFieldError('email', 'Invalid email');
72
+ state.setFieldError('name', 'Required');
73
+ expect(state.isValid.value).toBe(false);
74
+
75
+ state.clearFieldError('email');
76
+ state.clearFieldError('name');
77
+ expect(state.isValid.value).toBe(true);
78
+ });
79
+
80
+ it('clears all errors', () => {
81
+ const emit = vi.fn();
82
+ const state = useFormValidation({}, emit);
83
+
84
+ state.setFieldError('email', 'Invalid email');
85
+ state.setFieldError('name', 'Required');
86
+
87
+ state.clearErrors();
88
+ expect(state.errors.value).toEqual({});
89
+ expect(state.isValid.value).toBe(true);
90
+ });
91
+ });
92
+
93
+ describe('validate', () => {
94
+ it('returns true when there are no errors', () => {
95
+ const emit = vi.fn();
96
+ const state = useFormValidation({}, emit);
97
+
98
+ expect(state.validate()).toBe(true);
99
+ });
100
+
101
+ it('clears errors before validation', () => {
102
+ const emit = vi.fn();
103
+ const state = useFormValidation({}, emit);
104
+
105
+ state.setFieldError('name', 'Error');
106
+ state.validate();
107
+ expect(state.errors.value).toEqual({});
108
+ });
109
+ });
110
+
111
+ describe('handleSubmit', () => {
112
+ it('prevents default form submission', async () => {
113
+ const emit = vi.fn();
114
+ const state = useFormValidation({}, emit);
115
+ const event = new Event('submit');
116
+ const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
117
+
118
+ await state.handleSubmit(event);
119
+ expect(preventDefaultSpy).toHaveBeenCalled();
120
+ });
121
+
122
+ it('emits submit event', async () => {
123
+ const emit = vi.fn();
124
+ const state = useFormValidation({}, emit);
125
+ const event = new Event('submit');
126
+
127
+ await state.handleSubmit(event);
128
+ expect(emit).toHaveBeenCalledWith('submit', event);
129
+ });
130
+
131
+ it('executes callback with form data', async () => {
132
+ const emit = vi.fn();
133
+ const initialData = { name: 'John', email: 'john@test.com' };
134
+ const state = useFormValidation({ initialData }, emit);
135
+ const callback = vi.fn();
136
+ const event = new Event('submit');
137
+
138
+ await state.handleSubmit(event, callback);
139
+ expect(callback).toHaveBeenCalledWith(initialData);
140
+ });
141
+
142
+ it('sets isSubmitting during submission', async () => {
143
+ const emit = vi.fn();
144
+ const state = useFormValidation({}, emit);
145
+ const callback = vi.fn(async () => {
146
+ expect(state.isSubmitting.value).toBe(true);
147
+ await new Promise((resolve) => setTimeout(resolve, 10));
148
+ });
149
+ const event = new Event('submit');
150
+
151
+ await state.handleSubmit(event, callback);
152
+ expect(state.isSubmitting.value).toBe(false);
153
+ });
154
+
155
+ it('handles async callback errors', async () => {
156
+ const emit = vi.fn();
157
+ const state = useFormValidation({}, emit);
158
+ const error = new Error('Submission failed');
159
+ const callback = vi.fn(async () => {
160
+ throw error;
161
+ });
162
+ const event = new Event('submit');
163
+
164
+ await expect(state.handleSubmit(event, callback)).rejects.toThrow(
165
+ 'Submission failed'
166
+ );
167
+ expect(emit).toHaveBeenCalledWith('submit-error', error);
168
+ expect(state.isSubmitting.value).toBe(false);
169
+ });
170
+ });
171
+
172
+ describe('reset', () => {
173
+ it('resets form to initial state', () => {
174
+ const emit = vi.fn();
175
+ const initialData = { name: 'John', email: 'john@test.com' };
176
+ const state = useFormValidation({ initialData }, emit);
177
+
178
+ state.setFieldValue('name', 'Alice');
179
+ state.setFieldError('email', 'Invalid');
180
+ state.isSubmitting.value = true;
181
+
182
+ state.reset();
183
+ expect(state.formData.value).toEqual(initialData);
184
+ expect(state.errors.value).toEqual({});
185
+ expect(state.isValid.value).toBe(true);
186
+ expect(state.isSubmitting.value).toBe(false);
187
+ });
188
+
189
+ it('resets to empty object when no initial data', () => {
190
+ const emit = vi.fn();
191
+ const state = useFormValidation({}, emit);
192
+
193
+ state.setFieldValue('name', 'Alice');
194
+ state.reset();
195
+ expect(state.formData.value).toEqual({});
196
+ });
197
+ });
198
+ });