@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.
- package/README.md +119 -0
- package/dist/fabric.cjs.js +18109 -0
- package/dist/fabric.css +2180 -0
- package/dist/fabric.esm.js +18062 -0
- package/dist/fabric.min.js +18112 -0
- package/dist/types/components/atoms/FAvatar/FAvatar.test.d.ts +1 -0
- package/dist/types/components/atoms/FBadge/FBadge.test.d.ts +1 -0
- package/dist/types/components/atoms/FButton/FButton.test.d.ts +1 -0
- package/dist/types/components/atoms/FCheckbox/FCheckbox.test.d.ts +1 -0
- package/dist/types/components/atoms/FDivider/FDivider.test.d.ts +1 -0
- package/dist/types/components/atoms/FIcon/FIcon.test.d.ts +1 -0
- package/dist/types/components/atoms/FInput/FInput.test.d.ts +1 -0
- package/dist/types/components/atoms/FLoader/FLoader.test.d.ts +1 -0
- package/dist/types/components/atoms/FRadio/FRadio.test.d.ts +1 -0
- package/dist/types/components/atoms/FTextarea/FTextarea.test.d.ts +1 -0
- package/dist/types/components/atoms/FToggle/FToggle.test.d.ts +1 -0
- package/dist/types/components/atoms/FTypography/FTypography.test.d.ts +1 -0
- package/dist/types/components/atoms/index.d.ts +13 -0
- package/dist/types/components/molecules/FAccordionItem/FAccordionItem.test.d.ts +1 -0
- package/dist/types/components/molecules/FAlert/FAlert.test.d.ts +1 -0
- package/dist/types/components/molecules/FBreadcrumb/FBreadcrumb.test.d.ts +1 -0
- package/dist/types/components/molecules/FButtonGroup/FButtonGroup.test.d.ts +1 -0
- package/dist/types/components/molecules/FCard/FCard.test.d.ts +1 -0
- package/dist/types/components/molecules/FDatePicker/FDatePicker.test.d.ts +1 -0
- package/dist/types/components/molecules/FEmptyState/FEmptyState.test.d.ts +1 -0
- package/dist/types/components/molecules/FFilePreview/FFilePreview.test.d.ts +1 -0
- package/dist/types/components/molecules/FFormField/FFormField.test.d.ts +1 -0
- package/dist/types/components/molecules/FListItem/FListItem.test.d.ts +1 -0
- package/dist/types/components/molecules/FPagination/FPagination.test.d.ts +1 -0
- package/dist/types/components/molecules/FSearchBar/FSearchBar.test.d.ts +1 -0
- package/dist/types/components/molecules/FSelect/FSelect.test.d.ts +1 -0
- package/dist/types/components/molecules/FStatCard/FStatCard.test.d.ts +1 -0
- package/dist/types/components/molecules/FTabs/FTabs.test.d.ts +1 -0
- package/dist/types/components/molecules/FToast/FToast.test.d.ts +1 -0
- package/dist/types/components/molecules/index.d.ts +18 -0
- package/dist/types/components/organisms/FActivityFeed/FActivityFeed.test.d.ts +1 -0
- package/dist/types/components/organisms/FDataTable/FDataTable.test.d.ts +1 -0
- package/dist/types/components/organisms/FDrawer/FDrawer.test.d.ts +1 -0
- package/dist/types/components/organisms/FFileUpload/FFileUpload.test.d.ts +1 -0
- package/dist/types/components/organisms/FFilterSidebar/FFilterSidebar.test.d.ts +1 -0
- package/dist/types/components/organisms/FForm/FForm.test.d.ts +1 -0
- package/dist/types/components/organisms/FModal/FModal.test.d.ts +1 -0
- package/dist/types/components/organisms/FNavigationSidebar/FNavigationSidebar.test.d.ts +1 -0
- package/dist/types/components/organisms/FOnboardingStepper/FOnboardingStepper.test.d.ts +1 -0
- package/dist/types/components/organisms/FOnboardingStepper/FStepperProgress.test.d.ts +1 -0
- package/dist/types/components/organisms/FPageHeader/FPageHeader.test.d.ts +1 -0
- package/dist/types/components/organisms/FProfileSection/FProfileSection.test.d.ts +1 -0
- package/dist/types/components/organisms/FToastProvider/FToastProvider.test.d.ts +1 -0
- package/dist/types/components/organisms/FUserMenu/FUserMenu.test.d.ts +1 -0
- package/dist/types/components/organisms/index.d.ts +14 -0
- package/dist/types/components/utils/FThemeProvider.test.d.ts +1 -0
- package/dist/types/components/utils/index.d.ts +2 -0
- package/dist/types/components.d.ts +602 -0
- package/dist/types/composables/index.d.ts +12 -0
- package/dist/types/composables/useDataTableState.d.ts +106 -0
- package/dist/types/composables/useDataTableState.test.d.ts +1 -0
- package/dist/types/composables/useFormValidation.d.ts +49 -0
- package/dist/types/composables/useFormValidation.test.d.ts +1 -0
- package/dist/types/composables/useSidebarState.d.ts +65 -0
- package/dist/types/composables/useSidebarState.test.d.ts +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/types.d.ts +529 -0
- package/package.json +100 -0
- package/src/components/atoms/FAvatar/FAvatar.stories.js +100 -0
- package/src/components/atoms/FAvatar/FAvatar.test.ts +95 -0
- package/src/components/atoms/FAvatar/FAvatar.vue +190 -0
- package/src/components/atoms/FBadge/FBadge.stories.js +129 -0
- package/src/components/atoms/FBadge/FBadge.test.ts +93 -0
- package/src/components/atoms/FBadge/FBadge.vue +103 -0
- package/src/components/atoms/FButton/FButton.stories.js +122 -0
- package/src/components/atoms/FButton/FButton.test.ts +98 -0
- package/src/components/atoms/FButton/FButton.vue +147 -0
- package/src/components/atoms/FCheckbox/FCheckbox.stories.js +96 -0
- package/src/components/atoms/FCheckbox/FCheckbox.test.ts +64 -0
- package/src/components/atoms/FCheckbox/FCheckbox.vue +76 -0
- package/src/components/atoms/FDivider/FDivider.stories.js +104 -0
- package/src/components/atoms/FDivider/FDivider.test.ts +80 -0
- package/src/components/atoms/FDivider/FDivider.vue +117 -0
- package/src/components/atoms/FIcon/FIcon.stories.js +189 -0
- package/src/components/atoms/FIcon/FIcon.test.ts +99 -0
- package/src/components/atoms/FIcon/FIcon.vue +192 -0
- package/src/components/atoms/FInput/FInput.stories.js +119 -0
- package/src/components/atoms/FInput/FInput.test.ts +79 -0
- package/src/components/atoms/FInput/FInput.vue +88 -0
- package/src/components/atoms/FLoader/FLoader.stories.js +109 -0
- package/src/components/atoms/FLoader/FLoader.test.ts +66 -0
- package/src/components/atoms/FLoader/FLoader.vue +97 -0
- package/src/components/atoms/FRadio/FRadio.stories.js +105 -0
- package/src/components/atoms/FRadio/FRadio.test.ts +75 -0
- package/src/components/atoms/FRadio/FRadio.vue +119 -0
- package/src/components/atoms/FTextarea/FTextarea.stories.js +126 -0
- package/src/components/atoms/FTextarea/FTextarea.test.ts +94 -0
- package/src/components/atoms/FTextarea/FTextarea.vue +156 -0
- package/src/components/atoms/FToggle/FToggle.stories.js +108 -0
- package/src/components/atoms/FToggle/FToggle.test.ts +96 -0
- package/src/components/atoms/FToggle/FToggle.vue +123 -0
- package/src/components/atoms/FTypography/FTypography.stories.js +127 -0
- package/src/components/atoms/FTypography/FTypography.test.ts +93 -0
- package/src/components/atoms/FTypography/FTypography.vue +78 -0
- package/src/components/atoms/index.ts +27 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.stories.js +71 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.test.ts +61 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.vue +105 -0
- package/src/components/molecules/FAlert/FAlert.stories.js +87 -0
- package/src/components/molecules/FAlert/FAlert.test.ts +59 -0
- package/src/components/molecules/FAlert/FAlert.vue +108 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.stories.js +90 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.test.ts +76 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.vue +117 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.stories.js +82 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.test.ts +44 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.vue +31 -0
- package/src/components/molecules/FCard/FCard.stories.js +136 -0
- package/src/components/molecules/FCard/FCard.test.ts +87 -0
- package/src/components/molecules/FCard/FCard.vue +75 -0
- package/src/components/molecules/FDatePicker/FDatePicker.stories.js +305 -0
- package/src/components/molecules/FDatePicker/FDatePicker.test.ts +282 -0
- package/src/components/molecules/FDatePicker/FDatePicker.vue +750 -0
- package/src/components/molecules/FEmptyState/FEmptyState.stories.js +98 -0
- package/src/components/molecules/FEmptyState/FEmptyState.test.ts +82 -0
- package/src/components/molecules/FEmptyState/FEmptyState.vue +89 -0
- package/src/components/molecules/FFilePreview/FFilePreview.stories.js +130 -0
- package/src/components/molecules/FFilePreview/FFilePreview.test.ts +70 -0
- package/src/components/molecules/FFilePreview/FFilePreview.vue +125 -0
- package/src/components/molecules/FFormField/FFormField.stories.js +149 -0
- package/src/components/molecules/FFormField/FFormField.test.ts +85 -0
- package/src/components/molecules/FFormField/FFormField.vue +107 -0
- package/src/components/molecules/FListItem/FListItem.stories.js +158 -0
- package/src/components/molecules/FListItem/FListItem.test.ts +93 -0
- package/src/components/molecules/FListItem/FListItem.vue +113 -0
- package/src/components/molecules/FPagination/FPagination.stories.js +132 -0
- package/src/components/molecules/FPagination/FPagination.test.ts +79 -0
- package/src/components/molecules/FPagination/FPagination.vue +206 -0
- package/src/components/molecules/FSearchBar/FSearchBar.stories.js +129 -0
- package/src/components/molecules/FSearchBar/FSearchBar.test.ts +81 -0
- package/src/components/molecules/FSearchBar/FSearchBar.vue +180 -0
- package/src/components/molecules/FSelect/FSelect.stories.js +333 -0
- package/src/components/molecules/FSelect/FSelect.test.ts +478 -0
- package/src/components/molecules/FSelect/FSelect.vue +551 -0
- package/src/components/molecules/FStatCard/FStatCard.stories.js +144 -0
- package/src/components/molecules/FStatCard/FStatCard.test.ts +78 -0
- package/src/components/molecules/FStatCard/FStatCard.vue +106 -0
- package/src/components/molecules/FTabs/FTab.vue +63 -0
- package/src/components/molecules/FTabs/FTabs.stories.js +277 -0
- package/src/components/molecules/FTabs/FTabs.test.ts +264 -0
- package/src/components/molecules/FTabs/FTabs.vue +273 -0
- package/src/components/molecules/FToast/FToast.stories.js +150 -0
- package/src/components/molecules/FToast/FToast.test.ts +157 -0
- package/src/components/molecules/FToast/FToast.vue +283 -0
- package/src/components/molecules/index.ts +37 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.stories.js +217 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.test.ts +134 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.vue +589 -0
- package/src/components/organisms/FDataTable/FDataTable.stories.js +370 -0
- package/src/components/organisms/FDataTable/FDataTable.test.ts +248 -0
- package/src/components/organisms/FDataTable/FDataTable.vue +808 -0
- package/src/components/organisms/FDrawer/FDrawer.stories.js +296 -0
- package/src/components/organisms/FDrawer/FDrawer.test.ts +142 -0
- package/src/components/organisms/FDrawer/FDrawer.vue +303 -0
- package/src/components/organisms/FFileUpload/FFileUpload.stories.js +162 -0
- package/src/components/organisms/FFileUpload/FFileUpload.test.ts +103 -0
- package/src/components/organisms/FFileUpload/FFileUpload.vue +616 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.stories.js +161 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.test.ts +92 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.vue +458 -0
- package/src/components/organisms/FForm/FForm.stories.js +270 -0
- package/src/components/organisms/FForm/FForm.test.ts +63 -0
- package/src/components/organisms/FForm/FForm.vue +19 -0
- package/src/components/organisms/FModal/FModal.stories.js +227 -0
- package/src/components/organisms/FModal/FModal.test.ts +181 -0
- package/src/components/organisms/FModal/FModal.vue +319 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.stories.js +176 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.test.ts +95 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.vue +577 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.stories.js +197 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.test.ts +114 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.vue +212 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.stories.js +122 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.test.ts +130 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.vue +146 -0
- package/src/components/organisms/FPageHeader/FPageHeader.stories.js +142 -0
- package/src/components/organisms/FPageHeader/FPageHeader.test.ts +83 -0
- package/src/components/organisms/FPageHeader/FPageHeader.vue +241 -0
- package/src/components/organisms/FProfileSection/FProfileSection.stories.js +190 -0
- package/src/components/organisms/FProfileSection/FProfileSection.test.ts +85 -0
- package/src/components/organisms/FProfileSection/FProfileSection.vue +562 -0
- package/src/components/organisms/FToastProvider/FToastProvider.stories.js +290 -0
- package/src/components/organisms/FToastProvider/FToastProvider.test.ts +215 -0
- package/src/components/organisms/FToastProvider/FToastProvider.vue +214 -0
- package/src/components/organisms/FUserMenu/FUserMenu.stories.js +170 -0
- package/src/components/organisms/FUserMenu/FUserMenu.test.ts +102 -0
- package/src/components/organisms/FUserMenu/FUserMenu.vue +407 -0
- package/src/components/organisms/index.ts +29 -0
- package/src/components/utils/FThemeProvider.stories.js +236 -0
- package/src/components/utils/FThemeProvider.test.ts +244 -0
- package/src/components/utils/FThemeProvider.vue +191 -0
- package/src/components/utils/index.ts +3 -0
- package/src/components.d.ts +602 -0
- package/src/composables/README.md +233 -0
- package/src/composables/index.ts +25 -0
- package/src/composables/useDataTableState.test.ts +378 -0
- package/src/composables/useDataTableState.ts +361 -0
- package/src/composables/useFormValidation.test.ts +198 -0
- package/src/composables/useFormValidation.ts +178 -0
- package/src/composables/useSidebarState.test.ts +307 -0
- package/src/composables/useSidebarState.ts +201 -0
- package/src/env.d.ts +14 -0
- package/src/index.ts +167 -0
- package/src/styles/tailwind.css +173 -0
- 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>
|