@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,95 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import FNavigationSidebar from './FNavigationSidebar.vue';
4
+
5
+ describe('FNavigationSidebar', () => {
6
+ const items = [
7
+ { label: 'Dashboard', href: '/dashboard', icon: 'home' },
8
+ { label: 'Projects', href: '/projects', icon: 'folder' },
9
+ { label: 'Settings', href: '/settings', icon: 'cog' }
10
+ ];
11
+
12
+ it('renders correctly with required props', () => {
13
+ const wrapper = mount(FNavigationSidebar, {
14
+ propsData: { items }
15
+ });
16
+ expect(wrapper.find('nav').exists()).toBe(true);
17
+ });
18
+
19
+ it('displays navigation items', () => {
20
+ const wrapper = mount(FNavigationSidebar, {
21
+ propsData: { items }
22
+ });
23
+ expect(wrapper.text()).toContain('Dashboard');
24
+ expect(wrapper.text()).toContain('Projects');
25
+ });
26
+
27
+ it('marks active item', () => {
28
+ const itemsWithActive = [
29
+ ...items.slice(0, 1).map((i) => ({ ...i, active: true })),
30
+ ...items.slice(1)
31
+ ];
32
+ const wrapper = mount(FNavigationSidebar, {
33
+ propsData: { items: itemsWithActive }
34
+ });
35
+ expect(wrapper.exists()).toBe(true);
36
+ });
37
+
38
+ it('renders icons', () => {
39
+ const wrapper = mount(FNavigationSidebar, {
40
+ propsData: { items }
41
+ });
42
+ expect(wrapper.findComponent({ name: 'FIcon' }).exists()).toBe(true);
43
+ });
44
+
45
+ it('collapses when collapsed prop is true', () => {
46
+ const wrapper = mount(FNavigationSidebar, {
47
+ propsData: { items, collapsed: true }
48
+ });
49
+ expect(wrapper.exists()).toBe(true);
50
+ });
51
+
52
+ it('emits navigate event when item is clicked', async () => {
53
+ const wrapper = mount(FNavigationSidebar, {
54
+ propsData: { items }
55
+ });
56
+ const navItems = wrapper.findAll('a, button');
57
+ if (navItems.length > 0) {
58
+ await navItems[0].trigger('click');
59
+ expect(wrapper.emitted('navigate')).toBeTruthy();
60
+ }
61
+ });
62
+
63
+ it('renders header slot', () => {
64
+ const wrapper = mount(FNavigationSidebar, {
65
+ propsData: { items },
66
+ slots: { header: '<div>Header</div>' }
67
+ });
68
+ expect(wrapper.html()).toContain('Header');
69
+ });
70
+
71
+ it('renders footer slot', () => {
72
+ const wrapper = mount(FNavigationSidebar, {
73
+ propsData: { items },
74
+ slots: { footer: '<div>Footer</div>' }
75
+ });
76
+ expect(wrapper.html()).toContain('Footer');
77
+ });
78
+
79
+ it('renders nested items', () => {
80
+ const nestedItems = [
81
+ {
82
+ label: 'Section',
83
+ icon: 'folder',
84
+ children: [
85
+ { label: 'Sub Item 1', href: '/sub1' },
86
+ { label: 'Sub Item 2', href: '/sub2' }
87
+ ]
88
+ }
89
+ ];
90
+ const wrapper = mount(FNavigationSidebar, {
91
+ propsData: { items: nestedItems }
92
+ });
93
+ expect(wrapper.exists()).toBe(true);
94
+ });
95
+ });
@@ -0,0 +1,577 @@
1
+ <template>
2
+ <aside
3
+ :class="sidebarClasses"
4
+ :style="sidebarStyle"
5
+ role="navigation"
6
+ aria-label="Navigation principale"
7
+ >
8
+ <!-- Branding/Logo Section -->
9
+ <div :class="brandingClasses">
10
+ <slot name="branding">
11
+ <div class="flex items-center gap-3">
12
+ <slot name="logo" />
13
+ <f-typography
14
+ v-if="title && !collapsed"
15
+ variant="h6"
16
+ class="transition-opacity duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]"
17
+ >
18
+ {{ title }}
19
+ </f-typography>
20
+ </div>
21
+ </slot>
22
+ <f-button
23
+ v-if="collapsible"
24
+ variant="ghost"
25
+ size="small"
26
+ :aria-label="
27
+ collapsed ? 'Développer la navigation' : 'Réduire la navigation'
28
+ "
29
+ @click="toggleCollapsed"
30
+ >
31
+ <f-icon
32
+ :name="collapsed ? 'chevron-right' : 'chevron-left'"
33
+ size="sm"
34
+ />
35
+ </f-button>
36
+ </div>
37
+
38
+ <!-- Navigation Content -->
39
+ <nav class="flex-1 overflow-y-auto py-2">
40
+ <!-- All Navigation Items (including submenus) rendered in order -->
41
+ <template v-for="(item, index) in navigationItems">
42
+ <!-- Group Label -->
43
+ <div
44
+ v-if="item.type === 'group'"
45
+ :key="`nav-group-${index}`"
46
+ :class="groupLabelClasses"
47
+ >
48
+ <f-typography
49
+ v-if="!collapsed"
50
+ variant="overline"
51
+ class="text-neutral-500"
52
+ >
53
+ {{ item.label }}
54
+ </f-typography>
55
+ <f-divider v-else margin="sm" />
56
+ </div>
57
+
58
+ <!-- Divider -->
59
+ <f-divider
60
+ v-else-if="item.type === 'divider'"
61
+ :key="`nav-divider-${index}`"
62
+ margin="sm"
63
+ />
64
+
65
+ <!-- Submenu Item (with children) -->
66
+ <div
67
+ v-else-if="item.children && item.children.length > 0"
68
+ :key="`nav-submenu-${index}`"
69
+ class="nav-submenu"
70
+ >
71
+ <button
72
+ :class="getNavItemClasses(item, true)"
73
+ :aria-expanded="String(isSubmenuOpen(item))"
74
+ @click="toggleSubmenu(item)"
75
+ >
76
+ <span class="flex items-center gap-3 flex-1 min-w-0">
77
+ <f-icon
78
+ v-if="item.icon"
79
+ :name="item.icon"
80
+ size="md"
81
+ :class="getIconClasses(item)"
82
+ />
83
+ <span
84
+ v-if="!collapsed"
85
+ class="truncate transition-opacity duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]"
86
+ >
87
+ {{ item.label }}
88
+ </span>
89
+ </span>
90
+ <f-icon
91
+ v-if="!collapsed"
92
+ name="chevron-down"
93
+ size="sm"
94
+ :class="getChevronClasses(item)"
95
+ />
96
+ </button>
97
+
98
+ <!-- Submenu Children -->
99
+ <div
100
+ v-show="isSubmenuOpen(item) && !collapsed"
101
+ class="submenu-content"
102
+ >
103
+ <component
104
+ :is="getItemComponent(child)"
105
+ v-for="(child, childIndex) in item.children"
106
+ :key="`child-${index}-${childIndex}`"
107
+ :href="child.href"
108
+ :to="child.to"
109
+ :class="getChildItemClasses(child)"
110
+ @click="handleItemClick(child, $event)"
111
+ >
112
+ <span class="flex items-center gap-3 flex-1 min-w-0">
113
+ <f-icon
114
+ v-if="child.icon"
115
+ :name="child.icon"
116
+ size="sm"
117
+ :class="getIconClasses(child)"
118
+ />
119
+ <span class="truncate">{{ child.label }}</span>
120
+ </span>
121
+ <f-badge
122
+ v-if="child.badge"
123
+ :variant="child.badgeVariant || 'primary'"
124
+ size="small"
125
+ >
126
+ {{ child.badge }}
127
+ </f-badge>
128
+ </component>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- Regular Navigation Item -->
133
+ <component
134
+ :is="getItemComponent(item)"
135
+ v-else
136
+ :key="`nav-item-${index}`"
137
+ :href="item.href"
138
+ :to="item.to"
139
+ :class="getNavItemClasses(item)"
140
+ @click="handleItemClick(item, $event)"
141
+ >
142
+ <span class="flex items-center gap-3 flex-1 min-w-0">
143
+ <f-icon
144
+ v-if="item.icon"
145
+ :name="item.icon"
146
+ size="md"
147
+ :class="getIconClasses(item)"
148
+ />
149
+ <span
150
+ v-if="!collapsed"
151
+ class="truncate transition-opacity duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]"
152
+ >
153
+ {{ item.label }}
154
+ </span>
155
+ </span>
156
+ <f-badge
157
+ v-if="item.badge && !collapsed"
158
+ :variant="item.badgeVariant || 'primary'"
159
+ size="small"
160
+ >
161
+ {{ item.badge }}
162
+ </f-badge>
163
+ </component>
164
+ </template>
165
+
166
+ <!-- Custom Navigation Slot -->
167
+ <slot name="navigation" />
168
+ </nav>
169
+
170
+ <!-- Footer Section -->
171
+ <div v-if="$slots.footer || showThemeToggle" :class="footerClasses">
172
+ <slot name="footer">
173
+ <div
174
+ v-if="showThemeToggle"
175
+ class="flex items-center"
176
+ :class="collapsed ? 'justify-center' : 'justify-between'"
177
+ >
178
+ <f-typography v-if="!collapsed" variant="caption">
179
+ {{ themeToggleLabel }}
180
+ </f-typography>
181
+ <f-toggle
182
+ :value="isDarkMode"
183
+ :aria-label="themeToggleLabel"
184
+ @input="handleThemeToggle"
185
+ />
186
+ </div>
187
+ </slot>
188
+ </div>
189
+ </aside>
190
+ </template>
191
+
192
+ <script>
193
+ import FTypography from '../../atoms/FTypography/FTypography.vue';
194
+ import FButton from '../../atoms/FButton/FButton.vue';
195
+ import FIcon from '../../atoms/FIcon/FIcon.vue';
196
+ import FDivider from '../../atoms/FDivider/FDivider.vue';
197
+ import FToggle from '../../atoms/FToggle/FToggle.vue';
198
+ import FBadge from '../../atoms/FBadge/FBadge.vue';
199
+
200
+ export default {
201
+ name: 'FNavigationSidebar',
202
+ components: {
203
+ FTypography,
204
+ FButton,
205
+ FIcon,
206
+ FDivider,
207
+ FToggle,
208
+ FBadge
209
+ },
210
+ props: {
211
+ /**
212
+ * Controls the collapsed state of the sidebar.
213
+ * Use v-model:collapsed for two-way binding.
214
+ */
215
+ collapsed: {
216
+ type: Boolean,
217
+ default: false
218
+ },
219
+ /**
220
+ * Title displayed next to the logo when expanded
221
+ */
222
+ title: {
223
+ type: String,
224
+ default: ''
225
+ },
226
+ /**
227
+ * Width of the sidebar when expanded
228
+ */
229
+ width: {
230
+ type: String,
231
+ default: '256px'
232
+ },
233
+ /**
234
+ * Width of the sidebar when collapsed
235
+ */
236
+ collapsedWidth: {
237
+ type: String,
238
+ default: '64px'
239
+ },
240
+ /**
241
+ * Allow collapsing/expanding the sidebar
242
+ */
243
+ collapsible: {
244
+ type: Boolean,
245
+ default: true
246
+ },
247
+ /**
248
+ * Navigation items configuration
249
+ * Each item: { id, label, icon, href, to, children, badge, badgeVariant, disabled, type }
250
+ * type: 'link' (default) | 'group' | 'divider'
251
+ */
252
+ items: {
253
+ type: Array,
254
+ default: () => []
255
+ },
256
+ /**
257
+ * Current active route path for determining active state
258
+ */
259
+ activeRoute: {
260
+ type: String,
261
+ default: ''
262
+ },
263
+ /**
264
+ * Show theme toggle in footer
265
+ */
266
+ showThemeToggle: {
267
+ type: Boolean,
268
+ default: false
269
+ },
270
+ /**
271
+ * Current dark mode state
272
+ */
273
+ isDarkMode: {
274
+ type: Boolean,
275
+ default: false
276
+ },
277
+ /**
278
+ * Label for the theme toggle
279
+ */
280
+ themeToggleLabel: {
281
+ type: String,
282
+ default: 'Mode sombre'
283
+ },
284
+ /**
285
+ * Position of the sidebar
286
+ */
287
+ position: {
288
+ type: String,
289
+ default: 'left',
290
+ validator: (value) => ['left', 'right'].includes(value)
291
+ }
292
+ },
293
+ data() {
294
+ return {
295
+ openSubmenus: []
296
+ };
297
+ },
298
+ computed: {
299
+ /**
300
+ * Filtered navigation items (excluding invalid entries)
301
+ */
302
+ navigationItems() {
303
+ return this.items.filter(
304
+ (item) => item && (item.label || item.type === 'divider')
305
+ );
306
+ },
307
+ /**
308
+ * Main sidebar container classes
309
+ */
310
+ sidebarClasses() {
311
+ const baseClasses = 'flex flex-col h-full bg-white border-neutral-200';
312
+ const transitionClasses =
313
+ 'transition-all duration-[var(--transition-duration-slow)] ease-[var(--transition-easing-standard)]';
314
+ const borderClasses = this.position === 'left' ? 'border-r' : 'border-l';
315
+
316
+ return [baseClasses, transitionClasses, borderClasses]
317
+ .filter(Boolean)
318
+ .join(' ');
319
+ },
320
+ /**
321
+ * Sidebar inline styles
322
+ */
323
+ sidebarStyle() {
324
+ return {
325
+ width: this.collapsed ? this.collapsedWidth : this.width
326
+ };
327
+ },
328
+ /**
329
+ * Branding section classes
330
+ */
331
+ brandingClasses() {
332
+ const baseClasses = 'flex items-center border-b border-neutral-200';
333
+ const transitionClasses =
334
+ 'transition-all duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]';
335
+ const paddingClasses = this.collapsed
336
+ ? 'justify-center p-3'
337
+ : 'justify-between p-4';
338
+
339
+ return [baseClasses, transitionClasses, paddingClasses]
340
+ .filter(Boolean)
341
+ .join(' ');
342
+ },
343
+ /**
344
+ * Group label classes
345
+ */
346
+ groupLabelClasses() {
347
+ return this.collapsed ? 'px-2 py-1' : 'px-4 py-2 mt-2';
348
+ },
349
+ /**
350
+ * Footer section classes
351
+ */
352
+ footerClasses() {
353
+ const baseClasses = 'border-t border-neutral-200';
354
+ const transitionClasses =
355
+ 'transition-all duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]';
356
+ const paddingClasses = this.collapsed ? 'p-2' : 'p-4';
357
+
358
+ return [baseClasses, transitionClasses, paddingClasses]
359
+ .filter(Boolean)
360
+ .join(' ');
361
+ }
362
+ },
363
+ watch: {
364
+ /**
365
+ * Close submenus when sidebar is collapsed
366
+ */
367
+ collapsed(newValue) {
368
+ if (newValue) {
369
+ this.openSubmenus = [];
370
+ }
371
+ }
372
+ },
373
+ created() {
374
+ this.initializeOpenSubmenus();
375
+ },
376
+ methods: {
377
+ /**
378
+ * Initialize open submenus based on active route
379
+ */
380
+ initializeOpenSubmenus() {
381
+ if (!this.activeRoute) return;
382
+
383
+ this.items.forEach((item) => {
384
+ if (item.children && item.children.length > 0) {
385
+ const hasActiveChild = item.children.some((child) =>
386
+ this.isItemActive(child)
387
+ );
388
+ if (
389
+ hasActiveChild &&
390
+ !this.openSubmenus.includes(item.id || item.label)
391
+ ) {
392
+ this.openSubmenus.push(item.id || item.label);
393
+ }
394
+ }
395
+ });
396
+ },
397
+ /**
398
+ * Toggle sidebar collapsed state
399
+ */
400
+ toggleCollapsed() {
401
+ this.$emit('update:collapsed', !this.collapsed);
402
+ this.$emit('toggle', !this.collapsed);
403
+ },
404
+ /**
405
+ * Check if a submenu is open
406
+ */
407
+ isSubmenuOpen(item) {
408
+ const key = item.id || item.label;
409
+ return this.openSubmenus.includes(key);
410
+ },
411
+ /**
412
+ * Toggle submenu open state
413
+ */
414
+ toggleSubmenu(item) {
415
+ const key = item.id || item.label;
416
+ const index = this.openSubmenus.indexOf(key);
417
+
418
+ if (index === -1) {
419
+ this.openSubmenus.push(key);
420
+ } else {
421
+ this.openSubmenus.splice(index, 1);
422
+ }
423
+
424
+ this.$emit('submenu-toggle', { item, open: index === -1 });
425
+ },
426
+ /**
427
+ * Check if an item is currently active
428
+ */
429
+ isItemActive(item) {
430
+ if (!this.activeRoute) return false;
431
+
432
+ const itemPath = item.to || item.href;
433
+ if (!itemPath) return false;
434
+
435
+ // Exact match
436
+ if (this.activeRoute === itemPath) return true;
437
+
438
+ // For nested routes: check if active route starts with item path
439
+ // followed by '/' or end of string to avoid partial matches
440
+ // e.g., '/users' should not match '/user-settings'
441
+ if (itemPath !== '/') {
442
+ return (
443
+ this.activeRoute.startsWith(itemPath + '/') ||
444
+ this.activeRoute === itemPath
445
+ );
446
+ }
447
+
448
+ return false;
449
+ },
450
+ /**
451
+ * Check if a parent item has an active child
452
+ */
453
+ hasActiveChild(item) {
454
+ if (!item.children || item.children.length === 0) return false;
455
+ return item.children.some((child) => this.isItemActive(child));
456
+ },
457
+ /**
458
+ * Get the component to use for navigation items
459
+ */
460
+ getItemComponent(item) {
461
+ if (item.to) return 'router-link';
462
+ if (item.href) return 'a';
463
+ return 'button';
464
+ },
465
+ /**
466
+ * Get classes for navigation items
467
+ */
468
+ getNavItemClasses(item, isSubmenuTrigger = false) {
469
+ const isActive = this.isItemActive(item) || this.hasActiveChild(item);
470
+ const isDisabled = item.disabled;
471
+
472
+ const baseClasses = 'flex items-center w-full gap-3 text-sm font-medium';
473
+ const transitionClasses =
474
+ 'transition-colors duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]';
475
+ const paddingClasses = this.collapsed
476
+ ? 'justify-center px-3 py-3'
477
+ : 'px-4 py-3';
478
+ const hoverClasses = !isDisabled ? 'hover:bg-neutral-50' : '';
479
+ const activeClasses = isActive
480
+ ? 'bg-primary-50 text-primary-600'
481
+ : 'text-neutral-700';
482
+ const disabledClasses = isDisabled
483
+ ? 'opacity-50 cursor-not-allowed'
484
+ : 'cursor-pointer';
485
+ const focusClasses =
486
+ 'focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:ring-inset';
487
+
488
+ return [
489
+ baseClasses,
490
+ transitionClasses,
491
+ paddingClasses,
492
+ hoverClasses,
493
+ activeClasses,
494
+ disabledClasses,
495
+ focusClasses
496
+ ]
497
+ .filter(Boolean)
498
+ .join(' ');
499
+ },
500
+ /**
501
+ * Get classes for child items in submenus
502
+ */
503
+ getChildItemClasses(item) {
504
+ const isActive = this.isItemActive(item);
505
+ const isDisabled = item.disabled;
506
+
507
+ const baseClasses =
508
+ 'flex items-center w-full gap-3 pl-11 pr-4 py-2 text-sm';
509
+ const transitionClasses =
510
+ 'transition-colors duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]';
511
+ const hoverClasses = !isDisabled ? 'hover:bg-neutral-50' : '';
512
+ const activeClasses = isActive
513
+ ? 'bg-primary-50 text-primary-600 font-medium'
514
+ : 'text-neutral-600';
515
+ const disabledClasses = isDisabled
516
+ ? 'opacity-50 cursor-not-allowed'
517
+ : 'cursor-pointer';
518
+ const focusClasses =
519
+ 'focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:ring-inset';
520
+
521
+ return [
522
+ baseClasses,
523
+ transitionClasses,
524
+ hoverClasses,
525
+ activeClasses,
526
+ disabledClasses,
527
+ focusClasses
528
+ ]
529
+ .filter(Boolean)
530
+ .join(' ');
531
+ },
532
+ /**
533
+ * Get icon classes based on active state
534
+ */
535
+ getIconClasses(item) {
536
+ const isActive = this.isItemActive(item) || this.hasActiveChild(item);
537
+ return isActive ? 'text-primary-600' : 'text-neutral-400';
538
+ },
539
+ /**
540
+ * Get chevron classes for submenu indicators
541
+ */
542
+ getChevronClasses(item) {
543
+ const isOpen = this.isSubmenuOpen(item);
544
+ const baseClasses = 'text-neutral-400';
545
+ const transitionClasses =
546
+ 'transition-transform duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]';
547
+ const rotateClasses = isOpen ? 'rotate-180' : 'rotate-0';
548
+
549
+ return `${baseClasses} ${transitionClasses} ${rotateClasses}`;
550
+ },
551
+ /**
552
+ * Handle navigation item click
553
+ */
554
+ handleItemClick(item, event) {
555
+ if (item.disabled) {
556
+ event.preventDefault();
557
+ return;
558
+ }
559
+
560
+ this.$emit('navigate', item);
561
+
562
+ // For items without href/to (custom actions)
563
+ if (!item.href && !item.to) {
564
+ event.preventDefault();
565
+ this.$emit('item-click', item);
566
+ }
567
+ },
568
+ /**
569
+ * Handle theme toggle
570
+ */
571
+ handleThemeToggle(value) {
572
+ this.$emit('update:isDarkMode', value);
573
+ this.$emit('theme-change', value);
574
+ }
575
+ }
576
+ };
577
+ </script>