@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,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>