@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,134 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import FActivityFeed from './FActivityFeed.vue';
|
|
4
|
+
|
|
5
|
+
describe('FActivityFeed', () => {
|
|
6
|
+
const events = [
|
|
7
|
+
{
|
|
8
|
+
id: 1,
|
|
9
|
+
type: 'create',
|
|
10
|
+
title: 'Created item',
|
|
11
|
+
timestamp: new Date().toISOString()
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 2,
|
|
15
|
+
type: 'update',
|
|
16
|
+
title: 'Updated item',
|
|
17
|
+
timestamp: new Date().toISOString()
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 3,
|
|
21
|
+
type: 'comment',
|
|
22
|
+
title: 'New comment',
|
|
23
|
+
timestamp: new Date().toISOString()
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
it('renders correctly with default props', () => {
|
|
28
|
+
const wrapper = mount(FActivityFeed);
|
|
29
|
+
expect(wrapper.find('[role="feed"]').exists()).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('displays events', () => {
|
|
33
|
+
const wrapper = mount(FActivityFeed, {
|
|
34
|
+
propsData: { events }
|
|
35
|
+
});
|
|
36
|
+
expect(wrapper.text()).toContain('Created item');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sorts events by timestamp descending', () => {
|
|
40
|
+
const wrapper = mount(FActivityFeed, {
|
|
41
|
+
propsData: { events }
|
|
42
|
+
});
|
|
43
|
+
expect(wrapper.exists()).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('shows empty state when no events', () => {
|
|
47
|
+
const wrapper = mount(FActivityFeed, {
|
|
48
|
+
propsData: { events: [] }
|
|
49
|
+
});
|
|
50
|
+
expect(wrapper.findComponent({ name: 'FEmptyState' }).exists()).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('shows loader when loading', () => {
|
|
54
|
+
const wrapper = mount(FActivityFeed, {
|
|
55
|
+
propsData: { events, loading: true }
|
|
56
|
+
});
|
|
57
|
+
expect(wrapper.findComponent({ name: 'FLoader' }).exists()).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('shows load more button when hasMore is true', () => {
|
|
61
|
+
const wrapper = mount(FActivityFeed, {
|
|
62
|
+
propsData: { events, hasMore: true }
|
|
63
|
+
});
|
|
64
|
+
expect(wrapper.text()).toContain("Charger plus d'événements");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('emits load-more event when load more button is clicked', async () => {
|
|
68
|
+
const wrapper = mount(FActivityFeed, {
|
|
69
|
+
propsData: { events, hasMore: true }
|
|
70
|
+
});
|
|
71
|
+
const loadMoreBtn = wrapper.find('button[type="button"]');
|
|
72
|
+
if (loadMoreBtn.exists()) {
|
|
73
|
+
await loadMoreBtn.trigger('click');
|
|
74
|
+
expect(wrapper.emitted('load-more')).toBeTruthy();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('shows timeline when showTimeline is true', () => {
|
|
79
|
+
const wrapper = mount(FActivityFeed, {
|
|
80
|
+
propsData: { events, showTimeline: true }
|
|
81
|
+
});
|
|
82
|
+
expect(wrapper.exists()).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('emits event-click when event is clicked', async () => {
|
|
86
|
+
const wrapper = mount(FActivityFeed, {
|
|
87
|
+
propsData: { events, clickable: true }
|
|
88
|
+
});
|
|
89
|
+
const listItem = wrapper.findComponent({ name: 'FListItem' });
|
|
90
|
+
if (listItem.exists()) {
|
|
91
|
+
await listItem.trigger('click');
|
|
92
|
+
expect(wrapper.emitted('event-click')).toBeTruthy();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('displays custom empty state text', () => {
|
|
97
|
+
const wrapper = mount(FActivityFeed, {
|
|
98
|
+
propsData: {
|
|
99
|
+
events: [],
|
|
100
|
+
emptyTitle: 'Custom Title',
|
|
101
|
+
emptyDescription: 'Custom Description'
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
expect(wrapper.text()).toContain('Custom Title');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('renders RecycleScroller when virtual is enabled', () => {
|
|
108
|
+
const wrapper = mount(FActivityFeed, {
|
|
109
|
+
propsData: { events, virtual: true }
|
|
110
|
+
});
|
|
111
|
+
expect(wrapper.findComponent({ name: 'RecycleScroller' }).exists()).toBe(
|
|
112
|
+
true
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('renders regular list when virtual is disabled', () => {
|
|
117
|
+
const wrapper = mount(FActivityFeed, {
|
|
118
|
+
propsData: { events, virtual: false }
|
|
119
|
+
});
|
|
120
|
+
expect(wrapper.findComponent({ name: 'RecycleScroller' }).exists()).toBe(
|
|
121
|
+
false
|
|
122
|
+
);
|
|
123
|
+
// Should still render list items
|
|
124
|
+
expect(wrapper.findComponent({ name: 'FListItem' }).exists()).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('uses virtualItemHeight when virtual is enabled', () => {
|
|
128
|
+
const wrapper = mount(FActivityFeed, {
|
|
129
|
+
propsData: { events, virtual: true, virtualItemHeight: 120 }
|
|
130
|
+
});
|
|
131
|
+
const scroller = wrapper.findComponent({ name: 'RecycleScroller' });
|
|
132
|
+
expect(scroller.exists()).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="containerClasses" role="feed" aria-label="Fil d'activité">
|
|
3
|
+
<!-- Loading state at the top for new events -->
|
|
4
|
+
<div v-if="loadingNew" :class="loadingNewClasses">
|
|
5
|
+
<f-loader size="sm" label="Chargement des nouveaux événements" />
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<!-- Events list -->
|
|
9
|
+
<div v-if="sortedEvents.length > 0 && !virtual" :class="listClasses">
|
|
10
|
+
<div
|
|
11
|
+
v-for="(event, index) in sortedEvents"
|
|
12
|
+
:key="getEventKey(event, index)"
|
|
13
|
+
:class="eventContainerClasses"
|
|
14
|
+
>
|
|
15
|
+
<!-- Timeline indicator -->
|
|
16
|
+
<div v-if="showTimeline" :class="timelineClasses">
|
|
17
|
+
<div :class="timelineDotClasses(event)">
|
|
18
|
+
<f-icon
|
|
19
|
+
v-if="getEventIcon(event)"
|
|
20
|
+
:name="getEventIcon(event)"
|
|
21
|
+
size="xs"
|
|
22
|
+
:class="timelineIconClasses"
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
<div
|
|
26
|
+
v-if="index < sortedEvents.length - 1"
|
|
27
|
+
:class="timelineLineClasses"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Event content -->
|
|
32
|
+
<div :class="eventContentClasses">
|
|
33
|
+
<!-- Custom render slot for the event type -->
|
|
34
|
+
<slot :name="'event-' + event.type" :event="event" :index="index">
|
|
35
|
+
<!-- Default event rendering using FListItem -->
|
|
36
|
+
<f-list-item
|
|
37
|
+
:title="getEventTitle(event)"
|
|
38
|
+
:subtitle="getEventSubtitle(event)"
|
|
39
|
+
:clickable="clickable"
|
|
40
|
+
:truncate="truncateContent"
|
|
41
|
+
@click="handleEventClick(event)"
|
|
42
|
+
>
|
|
43
|
+
<template #left>
|
|
44
|
+
<div :class="eventIconContainerClasses(event)">
|
|
45
|
+
<f-icon :name="getEventIcon(event)" :size="iconSize" />
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<template #content>
|
|
50
|
+
<slot name="event-content" :event="event">
|
|
51
|
+
<div :class="eventBodyClasses">
|
|
52
|
+
<!-- Event description -->
|
|
53
|
+
<f-typography
|
|
54
|
+
v-if="event.description"
|
|
55
|
+
variant="body"
|
|
56
|
+
:class="descriptionClasses"
|
|
57
|
+
>
|
|
58
|
+
{{ event.description }}
|
|
59
|
+
</f-typography>
|
|
60
|
+
|
|
61
|
+
<!-- Event metadata: badge and timestamp -->
|
|
62
|
+
<div :class="metadataClasses">
|
|
63
|
+
<f-badge
|
|
64
|
+
v-if="getEventBadge(event)"
|
|
65
|
+
:variant="getEventBadge(event).variant || 'neutral'"
|
|
66
|
+
:content="getEventBadge(event).label"
|
|
67
|
+
size="sm"
|
|
68
|
+
/>
|
|
69
|
+
<f-typography variant="caption" :class="timestampClasses">
|
|
70
|
+
<f-icon name="clock" size="xs" class="mr-1" />
|
|
71
|
+
{{ formatTimestamp(event.timestamp) }}
|
|
72
|
+
</f-typography>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</slot>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<template #right>
|
|
79
|
+
<slot name="event-actions" :event="event" />
|
|
80
|
+
</template>
|
|
81
|
+
</f-list-item>
|
|
82
|
+
</slot>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Virtual events list -->
|
|
88
|
+
<div v-if="sortedEvents.length > 0 && virtual" :class="listClasses">
|
|
89
|
+
<RecycleScroller
|
|
90
|
+
:items="sortedEvents"
|
|
91
|
+
:item-size="virtualItemHeight"
|
|
92
|
+
:key-field="eventKey"
|
|
93
|
+
:buffer="200"
|
|
94
|
+
class="scroller"
|
|
95
|
+
:style="{ height: virtualHeight + 'px' }"
|
|
96
|
+
>
|
|
97
|
+
<template #default="{ item: event, index }">
|
|
98
|
+
<div :class="eventContainerClasses">
|
|
99
|
+
<!-- Timeline indicator -->
|
|
100
|
+
<div v-if="showTimeline" :class="timelineClasses">
|
|
101
|
+
<div :class="timelineDotClasses(event)">
|
|
102
|
+
<f-icon
|
|
103
|
+
v-if="getEventIcon(event)"
|
|
104
|
+
:name="getEventIcon(event)"
|
|
105
|
+
size="xs"
|
|
106
|
+
:class="timelineIconClasses"
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
<div
|
|
110
|
+
v-if="index < sortedEvents.length - 1"
|
|
111
|
+
:class="timelineLineClasses"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<!-- Event content -->
|
|
116
|
+
<div :class="eventContentClasses">
|
|
117
|
+
<!-- Custom render slot for the event type -->
|
|
118
|
+
<slot :name="'event-' + event.type" :event="event" :index="index">
|
|
119
|
+
<!-- Default event rendering using FListItem -->
|
|
120
|
+
<f-list-item
|
|
121
|
+
:title="getEventTitle(event)"
|
|
122
|
+
:subtitle="getEventSubtitle(event)"
|
|
123
|
+
:clickable="clickable"
|
|
124
|
+
:truncate="truncateContent"
|
|
125
|
+
@click="handleEventClick(event)"
|
|
126
|
+
>
|
|
127
|
+
<template #left>
|
|
128
|
+
<div :class="eventIconContainerClasses(event)">
|
|
129
|
+
<f-icon :name="getEventIcon(event)" :size="iconSize" />
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
|
+
|
|
133
|
+
<template #content>
|
|
134
|
+
<slot name="event-content" :event="event">
|
|
135
|
+
<div :class="eventBodyClasses">
|
|
136
|
+
<!-- Event description -->
|
|
137
|
+
<f-typography
|
|
138
|
+
v-if="event.description"
|
|
139
|
+
variant="body"
|
|
140
|
+
:class="descriptionClasses"
|
|
141
|
+
>
|
|
142
|
+
{{ event.description }}
|
|
143
|
+
</f-typography>
|
|
144
|
+
|
|
145
|
+
<!-- Event metadata: badge and timestamp -->
|
|
146
|
+
<div :class="metadataClasses">
|
|
147
|
+
<f-badge
|
|
148
|
+
v-if="getEventBadge(event)"
|
|
149
|
+
:variant="getEventBadge(event).variant || 'neutral'"
|
|
150
|
+
:content="getEventBadge(event).label"
|
|
151
|
+
size="sm"
|
|
152
|
+
/>
|
|
153
|
+
<f-typography
|
|
154
|
+
variant="caption"
|
|
155
|
+
:class="timestampClasses"
|
|
156
|
+
>
|
|
157
|
+
<f-icon name="clock" size="xs" class="mr-1" />
|
|
158
|
+
{{ formatTimestamp(event.timestamp) }}
|
|
159
|
+
</f-typography>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</slot>
|
|
163
|
+
</template>
|
|
164
|
+
|
|
165
|
+
<template #right>
|
|
166
|
+
<slot name="event-actions" :event="event" />
|
|
167
|
+
</template>
|
|
168
|
+
</f-list-item>
|
|
169
|
+
</slot>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</template>
|
|
173
|
+
</RecycleScroller>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<!-- Empty state -->
|
|
177
|
+
<f-empty-state
|
|
178
|
+
v-else-if="!loading"
|
|
179
|
+
:icon="emptyIcon"
|
|
180
|
+
:title="emptyTitle"
|
|
181
|
+
:description="emptyDescription"
|
|
182
|
+
:action-label="emptyActionLabel"
|
|
183
|
+
@action="$emit('empty-action')"
|
|
184
|
+
/>
|
|
185
|
+
|
|
186
|
+
<!-- Load more button / infinite scroll trigger -->
|
|
187
|
+
<div
|
|
188
|
+
v-if="sortedEvents.length > 0 && hasMore"
|
|
189
|
+
ref="loadMoreTrigger"
|
|
190
|
+
:class="loadMoreClasses"
|
|
191
|
+
>
|
|
192
|
+
<f-loader v-if="loading" size="md" :label="loadingLabel" />
|
|
193
|
+
<button
|
|
194
|
+
v-else-if="!infiniteScroll"
|
|
195
|
+
type="button"
|
|
196
|
+
:class="loadMoreButtonClasses"
|
|
197
|
+
@click="handleLoadMore"
|
|
198
|
+
>
|
|
199
|
+
{{ loadMoreLabel }}
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</template>
|
|
204
|
+
|
|
205
|
+
<script>
|
|
206
|
+
import FListItem from '../../molecules/FListItem/FListItem.vue';
|
|
207
|
+
import FEmptyState from '../../molecules/FEmptyState/FEmptyState.vue';
|
|
208
|
+
import FTypography from '../../atoms/FTypography/FTypography.vue';
|
|
209
|
+
import FIcon from '../../atoms/FIcon/FIcon.vue';
|
|
210
|
+
import FBadge from '../../atoms/FBadge/FBadge.vue';
|
|
211
|
+
import FLoader from '../../atoms/FLoader/FLoader.vue';
|
|
212
|
+
import { RecycleScroller } from 'vue-virtual-scroller';
|
|
213
|
+
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
|
|
214
|
+
|
|
215
|
+
export default {
|
|
216
|
+
name: 'FActivityFeed',
|
|
217
|
+
components: {
|
|
218
|
+
FListItem,
|
|
219
|
+
FEmptyState,
|
|
220
|
+
FTypography,
|
|
221
|
+
FIcon,
|
|
222
|
+
FBadge,
|
|
223
|
+
FLoader,
|
|
224
|
+
RecycleScroller
|
|
225
|
+
},
|
|
226
|
+
props: {
|
|
227
|
+
/**
|
|
228
|
+
* Array of event objects to display.
|
|
229
|
+
* Each event should have: { id, type, title, timestamp, description?, actor?, metadata? }
|
|
230
|
+
*/
|
|
231
|
+
events: {
|
|
232
|
+
type: Array,
|
|
233
|
+
default: () => []
|
|
234
|
+
},
|
|
235
|
+
/**
|
|
236
|
+
* Unique key property in event objects
|
|
237
|
+
*/
|
|
238
|
+
eventKey: {
|
|
239
|
+
type: String,
|
|
240
|
+
default: 'id'
|
|
241
|
+
},
|
|
242
|
+
/**
|
|
243
|
+
* Event type configurations for customizing icons and badges.
|
|
244
|
+
* Object format: { [type]: { icon: string, variant: string, label: string } }
|
|
245
|
+
*/
|
|
246
|
+
eventTypes: {
|
|
247
|
+
type: Object,
|
|
248
|
+
default: () => ({
|
|
249
|
+
comment: { icon: 'mail', variant: 'primary', label: 'Commentaire' },
|
|
250
|
+
status: { icon: 'info', variant: 'warning', label: 'Statut' },
|
|
251
|
+
create: { icon: 'plus', variant: 'success', label: 'Création' },
|
|
252
|
+
update: { icon: 'edit', variant: 'neutral', label: 'Modification' },
|
|
253
|
+
delete: { icon: 'trash', variant: 'error', label: 'Suppression' },
|
|
254
|
+
default: { icon: 'bell', variant: 'neutral', label: 'Événement' }
|
|
255
|
+
})
|
|
256
|
+
},
|
|
257
|
+
/**
|
|
258
|
+
* Whether the events list is currently loading
|
|
259
|
+
*/
|
|
260
|
+
loading: {
|
|
261
|
+
type: Boolean,
|
|
262
|
+
default: false
|
|
263
|
+
},
|
|
264
|
+
/**
|
|
265
|
+
* Whether new events are being loaded (for pull-to-refresh or new event polling)
|
|
266
|
+
*/
|
|
267
|
+
loadingNew: {
|
|
268
|
+
type: Boolean,
|
|
269
|
+
default: false
|
|
270
|
+
},
|
|
271
|
+
/**
|
|
272
|
+
* Whether there are more events to load
|
|
273
|
+
*/
|
|
274
|
+
hasMore: {
|
|
275
|
+
type: Boolean,
|
|
276
|
+
default: false
|
|
277
|
+
},
|
|
278
|
+
/**
|
|
279
|
+
* Enable infinite scroll to load more events
|
|
280
|
+
*/
|
|
281
|
+
infiniteScroll: {
|
|
282
|
+
type: Boolean,
|
|
283
|
+
default: false
|
|
284
|
+
},
|
|
285
|
+
/**
|
|
286
|
+
* Threshold in pixels from the bottom to trigger load more
|
|
287
|
+
*/
|
|
288
|
+
infiniteScrollThreshold: {
|
|
289
|
+
type: Number,
|
|
290
|
+
default: 100
|
|
291
|
+
},
|
|
292
|
+
/**
|
|
293
|
+
* Whether events are clickable
|
|
294
|
+
*/
|
|
295
|
+
clickable: {
|
|
296
|
+
type: Boolean,
|
|
297
|
+
default: false
|
|
298
|
+
},
|
|
299
|
+
/**
|
|
300
|
+
* Show timeline indicator on the left
|
|
301
|
+
*/
|
|
302
|
+
showTimeline: {
|
|
303
|
+
type: Boolean,
|
|
304
|
+
default: true
|
|
305
|
+
},
|
|
306
|
+
/**
|
|
307
|
+
* Truncate long content
|
|
308
|
+
*/
|
|
309
|
+
truncateContent: {
|
|
310
|
+
type: Boolean,
|
|
311
|
+
default: false
|
|
312
|
+
},
|
|
313
|
+
/**
|
|
314
|
+
* Icon size for event icons
|
|
315
|
+
*/
|
|
316
|
+
iconSize: {
|
|
317
|
+
type: String,
|
|
318
|
+
default: 'md',
|
|
319
|
+
validator: (value) => ['xs', 'sm', 'md', 'lg'].includes(value)
|
|
320
|
+
},
|
|
321
|
+
/**
|
|
322
|
+
* Date/time format function for timestamps
|
|
323
|
+
*/
|
|
324
|
+
formatTimestamp: {
|
|
325
|
+
type: Function,
|
|
326
|
+
default: (timestamp) => {
|
|
327
|
+
if (!timestamp) return '';
|
|
328
|
+
const date = new Date(timestamp);
|
|
329
|
+
if (isNaN(date.getTime())) return String(timestamp);
|
|
330
|
+
|
|
331
|
+
const now = new Date();
|
|
332
|
+
const diff = now - date;
|
|
333
|
+
const seconds = Math.floor(diff / 1000);
|
|
334
|
+
const minutes = Math.floor(seconds / 60);
|
|
335
|
+
const hours = Math.floor(minutes / 60);
|
|
336
|
+
const days = Math.floor(hours / 24);
|
|
337
|
+
|
|
338
|
+
if (seconds < 60) return "À l'instant";
|
|
339
|
+
if (minutes < 60) return `Il y a ${minutes} min`;
|
|
340
|
+
if (hours < 24) return `Il y a ${hours}h`;
|
|
341
|
+
if (days < 7) return `Il y a ${days}j`;
|
|
342
|
+
|
|
343
|
+
return date.toLocaleDateString('fr-FR', {
|
|
344
|
+
day: 'numeric',
|
|
345
|
+
month: 'short',
|
|
346
|
+
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
/**
|
|
351
|
+
* Empty state icon
|
|
352
|
+
*/
|
|
353
|
+
emptyIcon: {
|
|
354
|
+
type: String,
|
|
355
|
+
default: 'bell'
|
|
356
|
+
},
|
|
357
|
+
/**
|
|
358
|
+
* Empty state title
|
|
359
|
+
*/
|
|
360
|
+
emptyTitle: {
|
|
361
|
+
type: String,
|
|
362
|
+
default: 'Aucune activité'
|
|
363
|
+
},
|
|
364
|
+
/**
|
|
365
|
+
* Empty state description
|
|
366
|
+
*/
|
|
367
|
+
emptyDescription: {
|
|
368
|
+
type: String,
|
|
369
|
+
default: "Il n'y a aucun événement à afficher pour le moment."
|
|
370
|
+
},
|
|
371
|
+
/**
|
|
372
|
+
* Empty state action button label
|
|
373
|
+
*/
|
|
374
|
+
emptyActionLabel: {
|
|
375
|
+
type: String,
|
|
376
|
+
default: ''
|
|
377
|
+
},
|
|
378
|
+
/**
|
|
379
|
+
* Load more button label
|
|
380
|
+
*/
|
|
381
|
+
loadMoreLabel: {
|
|
382
|
+
type: String,
|
|
383
|
+
default: "Charger plus d'événements"
|
|
384
|
+
},
|
|
385
|
+
/**
|
|
386
|
+
* Loading label for accessibility
|
|
387
|
+
*/
|
|
388
|
+
loadingLabel: {
|
|
389
|
+
type: String,
|
|
390
|
+
default: 'Chargement en cours'
|
|
391
|
+
},
|
|
392
|
+
/**
|
|
393
|
+
* Enable virtualization for large event lists (improves performance with 1000+ events)
|
|
394
|
+
* When enabled, only visible events are rendered.
|
|
395
|
+
*/
|
|
396
|
+
virtual: {
|
|
397
|
+
type: Boolean,
|
|
398
|
+
default: false
|
|
399
|
+
},
|
|
400
|
+
/**
|
|
401
|
+
* Height of each virtualized event in pixels
|
|
402
|
+
* Used only when virtual is enabled
|
|
403
|
+
*/
|
|
404
|
+
virtualItemHeight: {
|
|
405
|
+
type: Number,
|
|
406
|
+
default: 100
|
|
407
|
+
},
|
|
408
|
+
/**
|
|
409
|
+
* Height of the virtual scroller container in pixels
|
|
410
|
+
* Used only when virtual is enabled
|
|
411
|
+
*/
|
|
412
|
+
virtualHeight: {
|
|
413
|
+
type: Number,
|
|
414
|
+
default: 600
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
data() {
|
|
418
|
+
return {
|
|
419
|
+
observer: null
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
computed: {
|
|
423
|
+
/**
|
|
424
|
+
* Sort events in descending chronological order (most recent first)
|
|
425
|
+
*/
|
|
426
|
+
sortedEvents() {
|
|
427
|
+
return [...this.events].sort((a, b) => {
|
|
428
|
+
const dateA = new Date(a.timestamp);
|
|
429
|
+
const dateB = new Date(b.timestamp);
|
|
430
|
+
return dateB - dateA;
|
|
431
|
+
});
|
|
432
|
+
},
|
|
433
|
+
containerClasses() {
|
|
434
|
+
return 'flex flex-col bg-white rounded-lg';
|
|
435
|
+
},
|
|
436
|
+
listClasses() {
|
|
437
|
+
return 'flex flex-col';
|
|
438
|
+
},
|
|
439
|
+
eventContainerClasses() {
|
|
440
|
+
return 'flex gap-3 relative';
|
|
441
|
+
},
|
|
442
|
+
eventContentClasses() {
|
|
443
|
+
return 'flex-1 min-w-0';
|
|
444
|
+
},
|
|
445
|
+
eventBodyClasses() {
|
|
446
|
+
return 'flex flex-col gap-2 mt-1';
|
|
447
|
+
},
|
|
448
|
+
descriptionClasses() {
|
|
449
|
+
return 'text-neutral-600 text-sm';
|
|
450
|
+
},
|
|
451
|
+
metadataClasses() {
|
|
452
|
+
return 'flex items-center gap-2 flex-wrap';
|
|
453
|
+
},
|
|
454
|
+
timestampClasses() {
|
|
455
|
+
return 'flex items-center text-neutral-400';
|
|
456
|
+
},
|
|
457
|
+
timelineClasses() {
|
|
458
|
+
return 'flex flex-col items-center flex-shrink-0 w-8';
|
|
459
|
+
},
|
|
460
|
+
timelineIconClasses() {
|
|
461
|
+
return 'text-white';
|
|
462
|
+
},
|
|
463
|
+
timelineLineClasses() {
|
|
464
|
+
return 'flex-1 w-0.5 bg-neutral-200 min-h-[24px]';
|
|
465
|
+
},
|
|
466
|
+
loadMoreClasses() {
|
|
467
|
+
return 'flex items-center justify-center py-4';
|
|
468
|
+
},
|
|
469
|
+
loadMoreButtonClasses() {
|
|
470
|
+
return 'px-4 py-2 text-sm font-medium text-primary-600 bg-primary-50 rounded-lg hover:bg-primary-100 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500/20';
|
|
471
|
+
},
|
|
472
|
+
loadingNewClasses() {
|
|
473
|
+
return 'flex items-center justify-center py-3 border-b border-neutral-100';
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
watch: {
|
|
477
|
+
infiniteScroll: {
|
|
478
|
+
handler(newVal) {
|
|
479
|
+
if (newVal) {
|
|
480
|
+
this.$nextTick(() => this.setupIntersectionObserver());
|
|
481
|
+
} else {
|
|
482
|
+
this.destroyIntersectionObserver();
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
immediate: true
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
mounted() {
|
|
489
|
+
if (this.infiniteScroll) {
|
|
490
|
+
this.setupIntersectionObserver();
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
beforeDestroy() {
|
|
494
|
+
this.destroyIntersectionObserver();
|
|
495
|
+
},
|
|
496
|
+
methods: {
|
|
497
|
+
getEventKey(event, index) {
|
|
498
|
+
return event[this.eventKey] ?? index;
|
|
499
|
+
},
|
|
500
|
+
getEventConfig(event) {
|
|
501
|
+
return this.eventTypes[event.type] || this.eventTypes.default || {};
|
|
502
|
+
},
|
|
503
|
+
getEventIcon(event) {
|
|
504
|
+
if (event.icon) return event.icon;
|
|
505
|
+
return this.getEventConfig(event).icon || 'bell';
|
|
506
|
+
},
|
|
507
|
+
getEventTitle(event) {
|
|
508
|
+
return event.title || '';
|
|
509
|
+
},
|
|
510
|
+
getEventSubtitle(event) {
|
|
511
|
+
if (event.actor) {
|
|
512
|
+
return `par ${event.actor}`;
|
|
513
|
+
}
|
|
514
|
+
return '';
|
|
515
|
+
},
|
|
516
|
+
getEventBadge(event) {
|
|
517
|
+
const config = this.getEventConfig(event);
|
|
518
|
+
if (event.badge) return event.badge;
|
|
519
|
+
if (config.label) {
|
|
520
|
+
return { variant: config.variant || 'neutral', label: config.label };
|
|
521
|
+
}
|
|
522
|
+
return null;
|
|
523
|
+
},
|
|
524
|
+
timelineDotClasses(event) {
|
|
525
|
+
const config = this.getEventConfig(event);
|
|
526
|
+
const variantClasses = {
|
|
527
|
+
primary: 'bg-primary-500',
|
|
528
|
+
success: 'bg-success-500',
|
|
529
|
+
warning: 'bg-warning-500',
|
|
530
|
+
error: 'bg-danger-500',
|
|
531
|
+
neutral: 'bg-neutral-400'
|
|
532
|
+
};
|
|
533
|
+
const bgClass = variantClasses[config.variant] || variantClasses.neutral;
|
|
534
|
+
return `w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${bgClass}`;
|
|
535
|
+
},
|
|
536
|
+
eventIconContainerClasses(event) {
|
|
537
|
+
const config = this.getEventConfig(event);
|
|
538
|
+
const variantClasses = {
|
|
539
|
+
primary: 'bg-primary-100 text-primary-600',
|
|
540
|
+
success: 'bg-success-100 text-success-600',
|
|
541
|
+
warning: 'bg-warning-100 text-warning-600',
|
|
542
|
+
error: 'bg-danger-100 text-danger-600',
|
|
543
|
+
neutral: 'bg-neutral-100 text-neutral-600'
|
|
544
|
+
};
|
|
545
|
+
const colorClass =
|
|
546
|
+
variantClasses[config.variant] || variantClasses.neutral;
|
|
547
|
+
return `w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${colorClass}`;
|
|
548
|
+
},
|
|
549
|
+
handleEventClick(event) {
|
|
550
|
+
if (this.clickable) {
|
|
551
|
+
this.$emit('event-click', event);
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
handleLoadMore() {
|
|
555
|
+
this.$emit('load-more');
|
|
556
|
+
},
|
|
557
|
+
setupIntersectionObserver() {
|
|
558
|
+
if (!('IntersectionObserver' in window)) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
this.$nextTick(() => {
|
|
563
|
+
const trigger = this.$refs.loadMoreTrigger;
|
|
564
|
+
if (!trigger) return;
|
|
565
|
+
|
|
566
|
+
this.observer = new IntersectionObserver(
|
|
567
|
+
(entries) => {
|
|
568
|
+
const entry = entries[0];
|
|
569
|
+
if (entry.isIntersecting && this.hasMore && !this.loading) {
|
|
570
|
+
this.$emit('load-more');
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
rootMargin: `${this.infiniteScrollThreshold}px`
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
this.observer.observe(trigger);
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
destroyIntersectionObserver() {
|
|
582
|
+
if (this.observer) {
|
|
583
|
+
this.observer.disconnect();
|
|
584
|
+
this.observer = null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
</script>
|