@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,178 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { ref, Ref } from 'vue';
3
+
4
+ /**
5
+ * Configuration options for useFormValidation
6
+ */
7
+ export interface FormValidationOptions {
8
+ /**
9
+ * Initial form data
10
+ */
11
+ initialData?: Record<string, any>;
12
+ }
13
+
14
+ /**
15
+ * Return type for useFormValidation
16
+ */
17
+ export interface FormValidationState {
18
+ // Form data
19
+ formData: Ref<Record<string, any>>;
20
+
21
+ // Validation state
22
+ errors: Ref<Record<string, string>>;
23
+ isValid: Ref<boolean>;
24
+ isSubmitting: Ref<boolean>;
25
+
26
+ // Methods
27
+ handleSubmit: (
28
+ event: Event,
29
+ callback?: (data: Record<string, any>) => void | Promise<void>
30
+ ) => Promise<void>;
31
+ setFieldValue: (field: string, value: any) => void;
32
+ setFieldError: (field: string, error: string) => void;
33
+ clearFieldError: (field: string) => void;
34
+ clearErrors: () => void;
35
+ reset: () => void;
36
+ validate: () => boolean;
37
+ }
38
+
39
+ /**
40
+ * Composable for managing form validation and submission
41
+ *
42
+ * Provides a simple interface for form handling with built-in validation state.
43
+ * Can be extended with custom validation logic.
44
+ *
45
+ * @param options - Configuration options for the form validation
46
+ * @param emit - Emit function from the component setup
47
+ * @returns Object containing reactive state and methods for form operations
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const formState = useFormValidation({
52
+ * initialData: { name: '', email: '' }
53
+ * }, emit);
54
+ *
55
+ * // In submit handler
56
+ * await formState.handleSubmit(event, async (data) => {
57
+ * await api.submitForm(data);
58
+ * });
59
+ * ```
60
+ */
61
+ export function useFormValidation(
62
+ options: FormValidationOptions = {},
63
+ emit: (event: string, ...args: any[]) => void
64
+ ): FormValidationState {
65
+ const { initialData = {} } = options;
66
+
67
+ // Reactive state
68
+ const formData = ref<Record<string, any>>({ ...initialData });
69
+ const errors = ref<Record<string, string>>({});
70
+ const isValid = ref(true);
71
+ const isSubmitting = ref(false);
72
+
73
+ /**
74
+ * Set a field value
75
+ */
76
+ const setFieldValue = (field: string, value: any): void => {
77
+ formData.value[field] = value;
78
+ // Clear error when field is updated
79
+ clearFieldError(field);
80
+ };
81
+
82
+ /**
83
+ * Set a field error
84
+ */
85
+ const setFieldError = (field: string, error: string): void => {
86
+ errors.value[field] = error;
87
+ isValid.value = false;
88
+ };
89
+
90
+ /**
91
+ * Clear a specific field error
92
+ */
93
+ const clearFieldError = (field: string): void => {
94
+ if (errors.value[field]) {
95
+ delete errors.value[field];
96
+ // Recalculate overall validity
97
+ isValid.value = Object.keys(errors.value).length === 0;
98
+ }
99
+ };
100
+
101
+ /**
102
+ * Clear all errors
103
+ */
104
+ const clearErrors = (): void => {
105
+ errors.value = {};
106
+ isValid.value = true;
107
+ };
108
+
109
+ /**
110
+ * Validate form (can be overridden with custom validation logic)
111
+ * Returns true if valid, false otherwise
112
+ */
113
+ const validate = (): boolean => {
114
+ clearErrors();
115
+ // Basic validation - can be extended with custom logic
116
+ // For now, just check if there are no errors
117
+ return isValid.value;
118
+ };
119
+
120
+ /**
121
+ * Handle form submission
122
+ */
123
+ const handleSubmit = async (
124
+ event: Event,
125
+ callback?: (data: Record<string, any>) => void | Promise<void>
126
+ ): Promise<void> => {
127
+ event.preventDefault();
128
+
129
+ // Emit submit event with the event object
130
+ emit('submit', event);
131
+
132
+ // If callback is provided, validate and execute it
133
+ if (callback) {
134
+ if (!validate()) {
135
+ return;
136
+ }
137
+
138
+ isSubmitting.value = true;
139
+ try {
140
+ await callback(formData.value);
141
+ } catch (error) {
142
+ // Emit error event if submission fails
143
+ emit('submit-error', error);
144
+ throw error;
145
+ } finally {
146
+ isSubmitting.value = false;
147
+ }
148
+ }
149
+ };
150
+
151
+ /**
152
+ * Reset form to initial state
153
+ */
154
+ const reset = (): void => {
155
+ formData.value = { ...initialData };
156
+ clearErrors();
157
+ isSubmitting.value = false;
158
+ };
159
+
160
+ return {
161
+ // Form data
162
+ formData,
163
+
164
+ // Validation state
165
+ errors,
166
+ isValid,
167
+ isSubmitting,
168
+
169
+ // Methods
170
+ handleSubmit,
171
+ setFieldValue,
172
+ setFieldError,
173
+ clearFieldError,
174
+ clearErrors,
175
+ reset,
176
+ validate
177
+ };
178
+ }
@@ -0,0 +1,307 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { useSidebarState } from './useSidebarState';
4
+
5
+ describe('useSidebarState', () => {
6
+ const items = [
7
+ { id: 'dashboard', label: 'Dashboard', href: '/dashboard', icon: 'home' },
8
+ { id: 'projects', label: 'Projects', href: '/projects', icon: 'folder' },
9
+ { id: 'settings', label: 'Settings', href: '/settings', icon: 'cog' }
10
+ ];
11
+
12
+ it('initializes with default state', () => {
13
+ const emit = vi.fn();
14
+ const state = useSidebarState({ items }, emit);
15
+
16
+ expect(state.collapsed.value).toBe(false);
17
+ expect(state.openSubmenus.value).toEqual([]);
18
+ });
19
+
20
+ it('initializes with custom collapsed state', () => {
21
+ const emit = vi.fn();
22
+ const state = useSidebarState({ items, initialCollapsed: true }, emit);
23
+
24
+ expect(state.collapsed.value).toBe(true);
25
+ });
26
+
27
+ describe('navigationItems', () => {
28
+ it('filters out invalid items', () => {
29
+ const emit = vi.fn();
30
+ const itemsWithInvalid = [
31
+ { id: 'dashboard', label: 'Dashboard', href: '/dashboard' },
32
+ null,
33
+ undefined,
34
+ { id: 'projects', label: 'Projects', href: '/projects' }
35
+ ];
36
+ const state = useSidebarState({ items: itemsWithInvalid as any }, emit);
37
+
38
+ expect(state.navigationItems.value.length).toBe(2);
39
+ });
40
+
41
+ it('includes divider items without labels', () => {
42
+ const emit = vi.fn();
43
+ const itemsWithDivider = [
44
+ { id: 'dashboard', label: 'Dashboard', href: '/dashboard' },
45
+ { type: 'divider' as const },
46
+ { id: 'projects', label: 'Projects', href: '/projects' }
47
+ ];
48
+ const state = useSidebarState({ items: itemsWithDivider }, emit);
49
+
50
+ expect(state.navigationItems.value.length).toBe(3);
51
+ });
52
+ });
53
+
54
+ describe('isItemActive', () => {
55
+ it('detects exact route match', () => {
56
+ const emit = vi.fn();
57
+ const state = useSidebarState({ items, activeRoute: '/dashboard' }, emit);
58
+
59
+ expect(state.isItemActive(items[0])).toBe(true);
60
+ expect(state.isItemActive(items[1])).toBe(false);
61
+ });
62
+
63
+ it('detects nested route match', () => {
64
+ const emit = vi.fn();
65
+ const state = useSidebarState(
66
+ { items, activeRoute: '/projects/123' },
67
+ emit
68
+ );
69
+
70
+ expect(state.isItemActive(items[1])).toBe(true);
71
+ });
72
+
73
+ it('does not match partial route names', () => {
74
+ const emit = vi.fn();
75
+ const itemsWithSimilarRoutes = [
76
+ { id: 'user', label: 'User', href: '/user' },
77
+ { id: 'users', label: 'Users', href: '/users' }
78
+ ];
79
+ const state = useSidebarState(
80
+ { items: itemsWithSimilarRoutes, activeRoute: '/users' },
81
+ emit
82
+ );
83
+
84
+ expect(state.isItemActive(itemsWithSimilarRoutes[0])).toBe(false);
85
+ expect(state.isItemActive(itemsWithSimilarRoutes[1])).toBe(true);
86
+ });
87
+
88
+ it('handles root path specially', () => {
89
+ const emit = vi.fn();
90
+ const itemsWithRoot = [
91
+ { id: 'home', label: 'Home', href: '/' },
92
+ { id: 'dashboard', label: 'Dashboard', href: '/dashboard' }
93
+ ];
94
+ const state = useSidebarState(
95
+ { items: itemsWithRoot, activeRoute: '/dashboard' },
96
+ emit
97
+ );
98
+
99
+ expect(state.isItemActive(itemsWithRoot[0])).toBe(false);
100
+ });
101
+
102
+ it('returns false when no active route is set', () => {
103
+ const emit = vi.fn();
104
+ const state = useSidebarState({ items }, emit);
105
+
106
+ expect(state.isItemActive(items[0])).toBe(false);
107
+ });
108
+
109
+ it('works with router-link "to" property', () => {
110
+ const emit = vi.fn();
111
+ const itemsWithTo = [
112
+ { id: 'dashboard', label: 'Dashboard', to: '/dashboard' }
113
+ ];
114
+ const state = useSidebarState(
115
+ { items: itemsWithTo, activeRoute: '/dashboard' },
116
+ emit
117
+ );
118
+
119
+ expect(state.isItemActive(itemsWithTo[0])).toBe(true);
120
+ });
121
+ });
122
+
123
+ describe('hasActiveChild', () => {
124
+ const nestedItems = [
125
+ {
126
+ id: 'section',
127
+ label: 'Section',
128
+ icon: 'folder',
129
+ children: [
130
+ { id: 'child1', label: 'Child 1', href: '/section/child1' },
131
+ { id: 'child2', label: 'Child 2', href: '/section/child2' }
132
+ ]
133
+ }
134
+ ];
135
+
136
+ it('detects active child', () => {
137
+ const emit = vi.fn();
138
+ const state = useSidebarState(
139
+ { items: nestedItems, activeRoute: '/section/child1' },
140
+ emit
141
+ );
142
+
143
+ expect(state.hasActiveChild(nestedItems[0])).toBe(true);
144
+ });
145
+
146
+ it('returns false when no child is active', () => {
147
+ const emit = vi.fn();
148
+ const state = useSidebarState(
149
+ { items: nestedItems, activeRoute: '/other' },
150
+ emit
151
+ );
152
+
153
+ expect(state.hasActiveChild(nestedItems[0])).toBe(false);
154
+ });
155
+
156
+ it('returns false when item has no children', () => {
157
+ const emit = vi.fn();
158
+ const state = useSidebarState({ items }, emit);
159
+
160
+ expect(state.hasActiveChild(items[0])).toBe(false);
161
+ });
162
+ });
163
+
164
+ describe('submenu management', () => {
165
+ const nestedItems = [
166
+ {
167
+ id: 'section',
168
+ label: 'Section',
169
+ children: [{ id: 'child1', label: 'Child 1', href: '/child1' }]
170
+ }
171
+ ];
172
+
173
+ it('checks if submenu is open', () => {
174
+ const emit = vi.fn();
175
+ const state = useSidebarState({ items: nestedItems }, emit);
176
+
177
+ expect(state.isSubmenuOpen(nestedItems[0])).toBe(false);
178
+
179
+ state.openSubmenus.value = ['section'];
180
+ expect(state.isSubmenuOpen(nestedItems[0])).toBe(true);
181
+ });
182
+
183
+ it('toggles submenu open', () => {
184
+ const emit = vi.fn();
185
+ const state = useSidebarState({ items: nestedItems }, emit);
186
+
187
+ state.toggleSubmenu(nestedItems[0]);
188
+ expect(state.isSubmenuOpen(nestedItems[0])).toBe(true);
189
+ expect(emit).toHaveBeenCalledWith('submenu-toggle', {
190
+ item: nestedItems[0],
191
+ open: true
192
+ });
193
+ });
194
+
195
+ it('toggles submenu closed', () => {
196
+ const emit = vi.fn();
197
+ const state = useSidebarState({ items: nestedItems }, emit);
198
+
199
+ state.openSubmenus.value = ['section'];
200
+ state.toggleSubmenu(nestedItems[0]);
201
+ expect(state.isSubmenuOpen(nestedItems[0])).toBe(false);
202
+ });
203
+
204
+ it('uses label as key when id is not present', () => {
205
+ const emit = vi.fn();
206
+ const itemsWithoutId = [
207
+ {
208
+ label: 'Section',
209
+ children: [{ label: 'Child', href: '/child' }]
210
+ }
211
+ ];
212
+ const state = useSidebarState({ items: itemsWithoutId }, emit);
213
+
214
+ state.toggleSubmenu(itemsWithoutId[0]);
215
+ expect(state.openSubmenus.value).toContain('Section');
216
+ });
217
+ });
218
+
219
+ describe('initializeOpenSubmenus', () => {
220
+ const nestedItems = [
221
+ {
222
+ id: 'section1',
223
+ label: 'Section 1',
224
+ children: [{ id: 'child1', label: 'Child 1', href: '/section1/child1' }]
225
+ },
226
+ {
227
+ id: 'section2',
228
+ label: 'Section 2',
229
+ children: [{ id: 'child2', label: 'Child 2', href: '/section2/child2' }]
230
+ }
231
+ ];
232
+
233
+ it('opens parent submenu when child is active', () => {
234
+ const emit = vi.fn();
235
+ const state = useSidebarState(
236
+ { items: nestedItems, activeRoute: '/section1/child1' },
237
+ emit
238
+ );
239
+
240
+ state.initializeOpenSubmenus();
241
+ expect(state.openSubmenus.value).toContain('section1');
242
+ expect(state.openSubmenus.value).not.toContain('section2');
243
+ });
244
+
245
+ it('does nothing when no active route', () => {
246
+ const emit = vi.fn();
247
+ const state = useSidebarState({ items: nestedItems }, emit);
248
+
249
+ state.initializeOpenSubmenus();
250
+ expect(state.openSubmenus.value).toEqual([]);
251
+ });
252
+
253
+ it('does not duplicate entries', () => {
254
+ const emit = vi.fn();
255
+ const state = useSidebarState(
256
+ { items: nestedItems, activeRoute: '/section1/child1' },
257
+ emit
258
+ );
259
+
260
+ state.initializeOpenSubmenus();
261
+ state.initializeOpenSubmenus();
262
+ expect(
263
+ state.openSubmenus.value.filter((k) => k === 'section1').length
264
+ ).toBe(1);
265
+ });
266
+ });
267
+
268
+ describe('toggleCollapsed', () => {
269
+ it('toggles collapsed state', () => {
270
+ const emit = vi.fn();
271
+ const state = useSidebarState({ items }, emit);
272
+
273
+ expect(state.collapsed.value).toBe(false);
274
+ state.toggleCollapsed();
275
+ expect(state.collapsed.value).toBe(true);
276
+ });
277
+
278
+ it('emits events on toggle', () => {
279
+ const emit = vi.fn();
280
+ const state = useSidebarState({ items }, emit);
281
+
282
+ state.toggleCollapsed();
283
+ expect(emit).toHaveBeenCalledWith('update:collapsed', true);
284
+ expect(emit).toHaveBeenCalledWith('toggle', true);
285
+ });
286
+
287
+ it('closes submenus when collapsed', async () => {
288
+ const emit = vi.fn();
289
+ const nestedItems = [
290
+ {
291
+ id: 'section',
292
+ label: 'Section',
293
+ children: [{ id: 'child', label: 'Child', href: '/child' }]
294
+ }
295
+ ];
296
+ const state = useSidebarState({ items: nestedItems }, emit);
297
+
298
+ state.toggleSubmenu(nestedItems[0]);
299
+ expect(state.openSubmenus.value.length).toBe(1);
300
+
301
+ state.toggleCollapsed();
302
+ // Wait for watcher to trigger
303
+ await new Promise((resolve) => setTimeout(resolve, 0));
304
+ expect(state.openSubmenus.value).toEqual([]);
305
+ });
306
+ });
307
+ });
@@ -0,0 +1,201 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { ref, computed, watch, Ref, ComputedRef } from 'vue';
3
+
4
+ /**
5
+ * Navigation item structure
6
+ */
7
+ export interface NavigationItem {
8
+ id?: string;
9
+ label?: string;
10
+ icon?: string;
11
+ href?: string;
12
+ to?: string;
13
+ children?: NavigationItem[];
14
+ badge?: string | number;
15
+ badgeVariant?: string;
16
+ disabled?: boolean;
17
+ type?: 'link' | 'group' | 'divider';
18
+ }
19
+
20
+ /**
21
+ * Configuration options for useSidebarState
22
+ */
23
+ export interface SidebarStateOptions {
24
+ /**
25
+ * Navigation items configuration
26
+ */
27
+ items: NavigationItem[];
28
+ /**
29
+ * Initial collapsed state
30
+ */
31
+ initialCollapsed?: boolean;
32
+ /**
33
+ * Current active route path
34
+ */
35
+ activeRoute?: string;
36
+ }
37
+
38
+ /**
39
+ * Return type for useSidebarState
40
+ */
41
+ export interface SidebarState {
42
+ // Collapse state
43
+ collapsed: Ref<boolean>;
44
+
45
+ // Submenu state
46
+ openSubmenus: Ref<string[]>;
47
+
48
+ // Filtered navigation items
49
+ navigationItems: ComputedRef<NavigationItem[]>;
50
+
51
+ // Methods
52
+ toggleCollapsed: () => void;
53
+ isSubmenuOpen: (item: NavigationItem) => boolean;
54
+ toggleSubmenu: (item: NavigationItem) => void;
55
+ isItemActive: (item: NavigationItem) => boolean;
56
+ hasActiveChild: (item: NavigationItem) => boolean;
57
+ initializeOpenSubmenus: () => void;
58
+ }
59
+
60
+ /**
61
+ * Composable for managing navigation sidebar state
62
+ *
63
+ * Handles collapsed state, submenu navigation, and active route detection.
64
+ *
65
+ * @param options - Configuration options for the sidebar state
66
+ * @param emit - Emit function from the component setup
67
+ * @returns Object containing reactive state and methods for sidebar operations
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const sidebarState = useSidebarState({
72
+ * items: navigationItems,
73
+ * activeRoute: '/dashboard'
74
+ * }, emit);
75
+ * ```
76
+ */
77
+ export function useSidebarState(
78
+ options: SidebarStateOptions,
79
+ emit: (event: string, ...args: any[]) => void
80
+ ): SidebarState {
81
+ const { items, initialCollapsed = false, activeRoute = '' } = options;
82
+
83
+ // Reactive state
84
+ const collapsed = ref(initialCollapsed);
85
+ const openSubmenus = ref<string[]>([]);
86
+
87
+ // Computed: Filtered navigation items (excluding invalid entries)
88
+ const navigationItems = computed(() => {
89
+ return items.filter(
90
+ (item) => item && (item.label || item.type === 'divider')
91
+ );
92
+ });
93
+
94
+ /**
95
+ * Check if an item is currently active
96
+ */
97
+ const isItemActive = (item: NavigationItem): boolean => {
98
+ if (!activeRoute) return false;
99
+
100
+ const itemPath = item.to || item.href;
101
+ if (!itemPath) return false;
102
+
103
+ // Exact match
104
+ if (activeRoute === itemPath) return true;
105
+
106
+ // For nested routes: check if active route starts with item path
107
+ // followed by '/' or end of string to avoid partial matches
108
+ // e.g., '/users' should not match '/user-settings'
109
+ if (itemPath !== '/') {
110
+ return activeRoute.startsWith(itemPath + '/') || activeRoute === itemPath;
111
+ }
112
+
113
+ return false;
114
+ };
115
+
116
+ /**
117
+ * Check if a parent item has an active child
118
+ */
119
+ const hasActiveChild = (item: NavigationItem): boolean => {
120
+ if (!item.children || item.children.length === 0) return false;
121
+ return item.children.some((child) => isItemActive(child));
122
+ };
123
+
124
+ /**
125
+ * Check if a submenu is open
126
+ */
127
+ const isSubmenuOpen = (item: NavigationItem): boolean => {
128
+ const key = item.id || item.label || '';
129
+ return openSubmenus.value.includes(key);
130
+ };
131
+
132
+ /**
133
+ * Toggle submenu open state
134
+ */
135
+ const toggleSubmenu = (item: NavigationItem): void => {
136
+ const key = item.id || item.label || '';
137
+ const index = openSubmenus.value.indexOf(key);
138
+
139
+ if (index === -1) {
140
+ openSubmenus.value.push(key);
141
+ } else {
142
+ openSubmenus.value.splice(index, 1);
143
+ }
144
+
145
+ emit('submenu-toggle', { item, open: index === -1 });
146
+ };
147
+
148
+ /**
149
+ * Toggle sidebar collapsed state
150
+ */
151
+ const toggleCollapsed = (): void => {
152
+ collapsed.value = !collapsed.value;
153
+ emit('update:collapsed', collapsed.value);
154
+ emit('toggle', collapsed.value);
155
+ };
156
+
157
+ /**
158
+ * Initialize open submenus based on active route
159
+ */
160
+ const initializeOpenSubmenus = (): void => {
161
+ if (!activeRoute) return;
162
+
163
+ items.forEach((item) => {
164
+ if (item.children && item.children.length > 0) {
165
+ const hasActiveChildItem = item.children.some((child) =>
166
+ isItemActive(child)
167
+ );
168
+ const key = item.id || item.label || '';
169
+ if (hasActiveChildItem && !openSubmenus.value.includes(key)) {
170
+ openSubmenus.value.push(key);
171
+ }
172
+ }
173
+ });
174
+ };
175
+
176
+ // Watch: Close submenus when sidebar is collapsed
177
+ watch(collapsed, (newValue) => {
178
+ if (newValue) {
179
+ openSubmenus.value = [];
180
+ }
181
+ });
182
+
183
+ return {
184
+ // Collapse state
185
+ collapsed,
186
+
187
+ // Submenu state
188
+ openSubmenus,
189
+
190
+ // Filtered navigation items
191
+ navigationItems,
192
+
193
+ // Methods
194
+ toggleCollapsed,
195
+ isSubmenuOpen,
196
+ toggleSubmenu,
197
+ isItemActive,
198
+ hasActiveChild,
199
+ initializeOpenSubmenus
200
+ };
201
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.vue' {
4
+ import type { DefineComponent } from 'vue';
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-explicit-any
6
+ const component: DefineComponent<{}, {}, any>;
7
+ export default component;
8
+ }
9
+
10
+ // Déclaration du module principal avec types enrichis
11
+ declare module '@pyreweb/fabric' {
12
+ export * from './components';
13
+ export * from './types';
14
+ }