@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,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import FBreadcrumb from './FBreadcrumb.vue';
4
+
5
+ describe('FBreadcrumb', () => {
6
+ const items = [
7
+ { label: 'Home', href: '/' },
8
+ { label: 'Products', href: '/products' },
9
+ { label: 'Current Page' }
10
+ ];
11
+
12
+ it('renders correctly with items', () => {
13
+ const wrapper = mount(FBreadcrumb, {
14
+ propsData: { items }
15
+ });
16
+ expect(wrapper.find('nav').exists()).toBe(true);
17
+ });
18
+
19
+ it('displays all items', () => {
20
+ const wrapper = mount(FBreadcrumb, {
21
+ propsData: { items }
22
+ });
23
+ expect(wrapper.text()).toContain('Home');
24
+ expect(wrapper.text()).toContain('Products');
25
+ expect(wrapper.text()).toContain('Current Page');
26
+ });
27
+
28
+ it('marks last item as current page', () => {
29
+ const wrapper = mount(FBreadcrumb, {
30
+ propsData: { items }
31
+ });
32
+ expect(wrapper.find('[aria-current="page"]').exists()).toBe(true);
33
+ });
34
+
35
+ it('renders links for non-current items', () => {
36
+ const wrapper = mount(FBreadcrumb, {
37
+ propsData: { items }
38
+ });
39
+ const links = wrapper.findAll('a');
40
+ expect(links.length).toBeGreaterThan(0);
41
+ });
42
+
43
+ it('shows separators between items', () => {
44
+ const wrapper = mount(FBreadcrumb, {
45
+ propsData: { items }
46
+ });
47
+ const icons = wrapper.findAllComponents({ name: 'FIcon' });
48
+ // Should have separators (number of items - 1)
49
+ expect(icons.length).toBeGreaterThanOrEqual(items.length - 1);
50
+ });
51
+
52
+ it('uses custom separator icon', () => {
53
+ const wrapper = mount(FBreadcrumb, {
54
+ propsData: { items, separatorIcon: 'arrow-right' }
55
+ });
56
+ expect(wrapper.exists()).toBe(true);
57
+ });
58
+
59
+ it('emits navigate event when item is clicked', async () => {
60
+ const wrapper = mount(FBreadcrumb, {
61
+ propsData: { items }
62
+ });
63
+ const buttons = wrapper.findAll('button');
64
+ if (buttons.length > 0) {
65
+ await buttons[0].trigger('click');
66
+ expect(wrapper.emitted('navigate')).toBeTruthy();
67
+ }
68
+ });
69
+
70
+ it('has correct aria-label', () => {
71
+ const wrapper = mount(FBreadcrumb, {
72
+ propsData: { items }
73
+ });
74
+ expect(wrapper.find('nav').attributes('aria-label')).toBe("Fil d'Ariane");
75
+ });
76
+ });
@@ -0,0 +1,117 @@
1
+ <template>
2
+ <nav :class="breadcrumbClasses" :aria-label="ariaLabel">
3
+ <ol class="flex items-center flex-wrap gap-1">
4
+ <li v-for="(item, index) in items" :key="index" class="flex items-center">
5
+ <!-- Separator (displayed before all items except the first) -->
6
+ <f-icon
7
+ v-if="index > 0"
8
+ :name="separatorIcon"
9
+ size="sm"
10
+ :class="separatorClasses"
11
+ />
12
+
13
+ <!-- Breadcrumb Item -->
14
+ <component
15
+ :is="isCurrentItem(index) ? 'span' : item.href ? 'a' : 'button'"
16
+ v-bind="!isCurrentItem(index) && item.href ? { href: item.href } : {}"
17
+ v-if="!isCurrentItem(index) || isCurrentItem(index)"
18
+ :class="getItemClasses(index)"
19
+ :aria-current="isCurrentItem(index) ? 'page' : undefined"
20
+ @click="handleItemClick($event, item, index)"
21
+ >
22
+ <f-icon v-if="item.icon" :name="item.icon" size="sm" class="mr-1" />
23
+ <f-typography
24
+ variant="body"
25
+ tag="span"
26
+ :class="getTextClasses(index)"
27
+ >
28
+ {{ item.label }}
29
+ </f-typography>
30
+ </component>
31
+ </li>
32
+ </ol>
33
+ </nav>
34
+ </template>
35
+
36
+ <script>
37
+ import FIcon from '../../atoms/FIcon/FIcon.vue';
38
+ import FTypography from '../../atoms/FTypography/FTypography.vue';
39
+
40
+ export default {
41
+ name: 'FBreadcrumb',
42
+ components: {
43
+ FIcon,
44
+ FTypography
45
+ },
46
+ props: {
47
+ items: {
48
+ type: Array,
49
+ required: true,
50
+ validator: (value) =>
51
+ value.every(
52
+ (item) =>
53
+ typeof item.label === 'string' &&
54
+ (item.href === undefined || typeof item.href === 'string') &&
55
+ (item.icon === undefined || typeof item.icon === 'string')
56
+ )
57
+ },
58
+ separatorIcon: {
59
+ type: String,
60
+ default: 'chevron-right'
61
+ },
62
+ ariaLabel: {
63
+ type: String,
64
+ default: "Fil d'Ariane"
65
+ }
66
+ },
67
+ computed: {
68
+ breadcrumbClasses() {
69
+ return 'inline-flex';
70
+ },
71
+ separatorClasses() {
72
+ return 'mx-2 text-neutral-400 flex-shrink-0';
73
+ }
74
+ },
75
+ methods: {
76
+ isCurrentItem(index) {
77
+ return index === this.items.length - 1;
78
+ },
79
+ getItemClasses(index) {
80
+ const baseClasses = 'inline-flex items-center';
81
+
82
+ if (this.isCurrentItem(index)) {
83
+ return [baseClasses, 'cursor-default'].join(' ');
84
+ }
85
+
86
+ return [
87
+ baseClasses,
88
+ 'cursor-pointer',
89
+ 'hover:underline',
90
+ 'focus:outline-none',
91
+ 'focus:ring-2',
92
+ 'focus:ring-primary-500/20',
93
+ 'focus:rounded'
94
+ ].join(' ');
95
+ },
96
+ getTextClasses(index) {
97
+ if (this.isCurrentItem(index)) {
98
+ return 'font-semibold text-neutral-800';
99
+ }
100
+ return 'text-primary-600 hover:text-primary-800';
101
+ },
102
+ handleItemClick(event, item, index) {
103
+ if (this.isCurrentItem(index)) {
104
+ event.preventDefault();
105
+ return;
106
+ }
107
+
108
+ this.$emit('navigate', { item, index, event });
109
+
110
+ // If the item has no href, prevent default and let the parent handle navigation
111
+ if (!item.href) {
112
+ event.preventDefault();
113
+ }
114
+ }
115
+ }
116
+ };
117
+ </script>
@@ -0,0 +1,82 @@
1
+ import FButtonGroup from './FButtonGroup.vue';
2
+ import FButton from '../../atoms/FButton/FButton.vue';
3
+
4
+ export default {
5
+ title: 'Molecules/FButtonGroup',
6
+ component: FButtonGroup,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ ariaLabel: {
10
+ control: 'text',
11
+ description: "Label pour l'accessibilité"
12
+ }
13
+ }
14
+ };
15
+
16
+ export const Default = () => ({
17
+ components: { FButtonGroup, FButton },
18
+ template: `
19
+ <FButtonGroup>
20
+ <FButton variant="outline">Gauche</FButton>
21
+ <FButton variant="outline">Centre</FButton>
22
+ <FButton variant="outline">Droite</FButton>
23
+ </FButtonGroup>
24
+ `
25
+ });
26
+
27
+ export const Primary = () => ({
28
+ components: { FButtonGroup, FButton },
29
+ template: `
30
+ <FButtonGroup>
31
+ <FButton variant="primary">Option 1</FButton>
32
+ <FButton variant="primary">Option 2</FButton>
33
+ <FButton variant="primary">Option 3</FButton>
34
+ </FButtonGroup>
35
+ `
36
+ });
37
+
38
+ export const Secondary = () => ({
39
+ components: { FButtonGroup, FButton },
40
+ template: `
41
+ <FButtonGroup>
42
+ <FButton variant="secondary">Jour</FButton>
43
+ <FButton variant="secondary">Semaine</FButton>
44
+ <FButton variant="secondary">Mois</FButton>
45
+ <FButton variant="secondary">Année</FButton>
46
+ </FButtonGroup>
47
+ `
48
+ });
49
+
50
+ export const WithActive = () => ({
51
+ components: { FButtonGroup, FButton },
52
+ data() {
53
+ return { active: 'jour' };
54
+ },
55
+ template: `
56
+ <FButtonGroup>
57
+ <FButton :variant="active === 'jour' ? 'primary' : 'outline'" @click="active = 'jour'">Jour</FButton>
58
+ <FButton :variant="active === 'semaine' ? 'primary' : 'outline'" @click="active = 'semaine'">Semaine</FButton>
59
+ <FButton :variant="active === 'mois' ? 'primary' : 'outline'" @click="active = 'mois'">Mois</FButton>
60
+ </FButtonGroup>
61
+ `
62
+ });
63
+
64
+ export const Sizes = () => ({
65
+ components: { FButtonGroup, FButton },
66
+ template: `
67
+ <div class="flex flex-col gap-4 items-start">
68
+ <FButtonGroup>
69
+ <FButton variant="outline" size="sm">Petit</FButton>
70
+ <FButton variant="outline" size="sm">Petit</FButton>
71
+ </FButtonGroup>
72
+ <FButtonGroup>
73
+ <FButton variant="outline" size="md">Moyen</FButton>
74
+ <FButton variant="outline" size="md">Moyen</FButton>
75
+ </FButtonGroup>
76
+ <FButtonGroup>
77
+ <FButton variant="outline" size="lg">Grand</FButton>
78
+ <FButton variant="outline" size="lg">Grand</FButton>
79
+ </FButtonGroup>
80
+ </div>
81
+ `
82
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import FButtonGroup from './FButtonGroup.vue';
4
+
5
+ describe('FButtonGroup', () => {
6
+ it('renders correctly with default props', () => {
7
+ const wrapper = mount(FButtonGroup, {
8
+ slots: {
9
+ default: '<button>Button 1</button><button>Button 2</button>'
10
+ }
11
+ });
12
+ expect(wrapper.find('[role="group"]').exists()).toBe(true);
13
+ });
14
+
15
+ it('displays slot content', () => {
16
+ const wrapper = mount(FButtonGroup, {
17
+ slots: {
18
+ default: '<button>Test Button</button>'
19
+ }
20
+ });
21
+ expect(wrapper.text()).toContain('Test Button');
22
+ });
23
+
24
+ it('has correct aria-label', () => {
25
+ const wrapper = mount(FButtonGroup, {
26
+ propsData: { ariaLabel: 'Custom Label' }
27
+ });
28
+ expect(wrapper.find('[role="group"]').attributes('aria-label')).toBe(
29
+ 'Custom Label'
30
+ );
31
+ });
32
+
33
+ it('has default aria-label', () => {
34
+ const wrapper = mount(FButtonGroup);
35
+ expect(wrapper.find('[role="group"]').attributes('aria-label')).toBe(
36
+ 'Groupe de boutons'
37
+ );
38
+ });
39
+
40
+ it('applies container classes', () => {
41
+ const wrapper = mount(FButtonGroup);
42
+ expect(wrapper.find('[role="group"]').classes()).toContain('inline-flex');
43
+ });
44
+ });
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div :class="containerClasses" role="group" :aria-label="ariaLabel">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script>
8
+ export default {
9
+ name: 'FButtonGroup',
10
+ props: {
11
+ ariaLabel: {
12
+ type: String,
13
+ default: 'Groupe de boutons'
14
+ }
15
+ },
16
+ computed: {
17
+ containerClasses() {
18
+ const baseClasses = 'inline-flex';
19
+ const childClasses = [
20
+ '[&>*]:rounded-none',
21
+ '[&>*:first-child]:rounded-l',
22
+ '[&>*:last-child]:rounded-r',
23
+ '[&>*:not(:first-child)]:-ml-px',
24
+ '[&>*]:focus:z-10'
25
+ ].join(' ');
26
+
27
+ return `${baseClasses} ${childClasses}`;
28
+ }
29
+ }
30
+ };
31
+ </script>
@@ -0,0 +1,136 @@
1
+ import FCard from './FCard.vue';
2
+ import FButton from '../../atoms/FButton/FButton.vue';
3
+
4
+ export default {
5
+ title: 'Molecules/FCard',
6
+ component: FCard,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ title: {
10
+ control: 'text',
11
+ description: 'Titre de la carte'
12
+ },
13
+ subtitle: {
14
+ control: 'text',
15
+ description: 'Sous-titre de la carte'
16
+ },
17
+ clickable: {
18
+ control: 'boolean',
19
+ description: 'Carte cliquable'
20
+ },
21
+ bordered: {
22
+ control: 'boolean',
23
+ description: 'Afficher une bordure'
24
+ }
25
+ }
26
+ };
27
+
28
+ const Template = (args, { argTypes }) => ({
29
+ components: { FCard },
30
+ props: Object.keys(argTypes),
31
+ template: `
32
+ <FCard v-bind="$props">
33
+ <p>Contenu de la carte.</p>
34
+ </FCard>
35
+ `
36
+ });
37
+
38
+ export const Default = Template.bind({});
39
+ Default.args = {
40
+ title: 'Titre de la carte',
41
+ subtitle: 'Sous-titre optionnel'
42
+ };
43
+
44
+ export const Simple = () => ({
45
+ components: { FCard },
46
+ template: `
47
+ <FCard>
48
+ <p>Une carte simple avec uniquement du contenu.</p>
49
+ </FCard>
50
+ `
51
+ });
52
+
53
+ export const WithActions = () => ({
54
+ components: { FCard, FButton },
55
+ template: `
56
+ <FCard title="Confirmation" subtitle="Action requise">
57
+ <p>Êtes-vous sûr de vouloir continuer ?</p>
58
+ <template #actions>
59
+ <FButton variant="outline">Annuler</FButton>
60
+ <FButton variant="primary">Confirmer</FButton>
61
+ </template>
62
+ </FCard>
63
+ `
64
+ });
65
+
66
+ export const WithMedia = () => ({
67
+ components: { FCard, FButton },
68
+ template: `
69
+ <FCard title="Article de blog" subtitle="Publié le 12 janvier">
70
+ <template #media>
71
+ <img src="https://picsum.photos/400/200" alt="Image de démonstration" />
72
+ </template>
73
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
74
+ <template #actions>
75
+ <FButton variant="link">Lire la suite</FButton>
76
+ </template>
77
+ </FCard>
78
+ `
79
+ });
80
+
81
+ export const Clickable = () => ({
82
+ components: { FCard },
83
+ methods: {
84
+ handleClick() {
85
+ alert('Carte cliquée !');
86
+ }
87
+ },
88
+ template: `
89
+ <FCard title="Carte cliquable" clickable @click="handleClick">
90
+ <p>Cliquez sur cette carte pour déclencher une action.</p>
91
+ </FCard>
92
+ `
93
+ });
94
+
95
+ export const NoBorder = () => ({
96
+ components: { FCard },
97
+ template: `
98
+ <div class="bg-neutral-100 p-4">
99
+ <FCard title="Sans bordure" :bordered="false">
100
+ <p>Cette carte n'a pas de bordure visible.</p>
101
+ </FCard>
102
+ </div>
103
+ `
104
+ });
105
+
106
+ export const CustomHeader = () => ({
107
+ components: { FCard, FButton },
108
+ template: `
109
+ <FCard>
110
+ <template #header>
111
+ <div class="flex justify-between items-center">
112
+ <h3 class="font-bold text-lg">En-tête personnalisé</h3>
113
+ <FButton variant="ghost" size="sm">Action</FButton>
114
+ </div>
115
+ </template>
116
+ <p>Contenu de la carte avec un en-tête personnalisé.</p>
117
+ </FCard>
118
+ `
119
+ });
120
+
121
+ export const Grid = () => ({
122
+ components: { FCard },
123
+ template: `
124
+ <div class="grid grid-cols-3 gap-4">
125
+ <FCard title="Carte 1">
126
+ <p>Contenu de la première carte.</p>
127
+ </FCard>
128
+ <FCard title="Carte 2">
129
+ <p>Contenu de la deuxième carte.</p>
130
+ </FCard>
131
+ <FCard title="Carte 3">
132
+ <p>Contenu de la troisième carte.</p>
133
+ </FCard>
134
+ </div>
135
+ `
136
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import FCard from './FCard.vue';
4
+
5
+ describe('FCard', () => {
6
+ it('renders correctly with default props', () => {
7
+ const wrapper = mount(FCard, {
8
+ slots: { default: 'Card content' }
9
+ });
10
+ expect(wrapper.text()).toContain('Card content');
11
+ });
12
+
13
+ it('displays title when provided', () => {
14
+ const wrapper = mount(FCard, {
15
+ propsData: { title: 'Card Title' }
16
+ });
17
+ expect(wrapper.text()).toContain('Card Title');
18
+ });
19
+
20
+ it('displays subtitle when provided', () => {
21
+ const wrapper = mount(FCard, {
22
+ propsData: { title: 'Title', subtitle: 'Subtitle text' }
23
+ });
24
+ expect(wrapper.text()).toContain('Subtitle text');
25
+ });
26
+
27
+ it('renders slot content', () => {
28
+ const wrapper = mount(FCard, {
29
+ slots: { default: '<p>Content paragraph</p>' }
30
+ });
31
+ expect(wrapper.html()).toContain('Content paragraph');
32
+ });
33
+
34
+ it('renders header slot', () => {
35
+ const wrapper = mount(FCard, {
36
+ slots: { header: '<h2>Custom Header</h2>' }
37
+ });
38
+ expect(wrapper.html()).toContain('Custom Header');
39
+ });
40
+
41
+ it('renders actions slot', () => {
42
+ const wrapper = mount(FCard, {
43
+ slots: { actions: '<button>Action</button>' }
44
+ });
45
+ expect(wrapper.html()).toContain('Action');
46
+ });
47
+
48
+ it('renders media slot', () => {
49
+ const wrapper = mount(FCard, {
50
+ slots: { media: '<img src="test.jpg" />' }
51
+ });
52
+ expect(wrapper.html()).toContain('test.jpg');
53
+ });
54
+
55
+ it('applies bordered class by default', () => {
56
+ const wrapper = mount(FCard);
57
+ expect(wrapper.classes()).toContain('border');
58
+ });
59
+
60
+ it('removes border when bordered is false', () => {
61
+ const wrapper = mount(FCard, {
62
+ propsData: { bordered: false }
63
+ });
64
+ expect(wrapper.classes()).not.toContain('border');
65
+ });
66
+
67
+ it('applies clickable styles when clickable', () => {
68
+ const wrapper = mount(FCard, {
69
+ propsData: { clickable: true }
70
+ });
71
+ expect(wrapper.classes()).toContain('cursor-pointer');
72
+ });
73
+
74
+ it('emits click event when clickable and clicked', async () => {
75
+ const wrapper = mount(FCard, {
76
+ propsData: { clickable: true }
77
+ });
78
+ await wrapper.trigger('click');
79
+ expect(wrapper.emitted('click')).toBeTruthy();
80
+ });
81
+
82
+ it('does not emit click when not clickable', async () => {
83
+ const wrapper = mount(FCard);
84
+ await wrapper.trigger('click');
85
+ expect(wrapper.emitted('click')).toBeFalsy();
86
+ });
87
+ });
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <div :class="cardClasses" @click="handleClick">
3
+ <div v-if="$slots.header || title" class="px-4 pt-4">
4
+ <slot name="header">
5
+ <f-typography v-if="title" variant="h5">{{ title }}</f-typography>
6
+ <f-typography v-if="subtitle" variant="caption">{{
7
+ subtitle
8
+ }}</f-typography>
9
+ </slot>
10
+ </div>
11
+ <div
12
+ v-if="$slots.media"
13
+ class="w-full [&_img]:w-full [&_img]:h-auto [&_img]:block"
14
+ >
15
+ <slot name="media" />
16
+ </div>
17
+ <div class="p-4">
18
+ <slot />
19
+ </div>
20
+ <div v-if="$slots.actions" class="px-4 pb-4 flex gap-2">
21
+ <slot name="actions" />
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <script>
27
+ import FTypography from '../../atoms/FTypography/FTypography.vue';
28
+
29
+ export default {
30
+ name: 'FCard',
31
+ components: {
32
+ FTypography
33
+ },
34
+ props: {
35
+ title: {
36
+ type: String,
37
+ default: ''
38
+ },
39
+ subtitle: {
40
+ type: String,
41
+ default: ''
42
+ },
43
+ clickable: {
44
+ type: Boolean,
45
+ default: false
46
+ },
47
+ bordered: {
48
+ type: Boolean,
49
+ default: true
50
+ }
51
+ },
52
+ computed: {
53
+ cardClasses() {
54
+ const baseClasses = 'bg-white rounded-lg overflow-hidden';
55
+ const transitionClasses =
56
+ 'transition-all duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]';
57
+ const borderedClasses = this.bordered ? 'border border-neutral-200' : '';
58
+ const clickableClasses = this.clickable
59
+ ? 'cursor-pointer hover:shadow-lg hover:-translate-y-0.5'
60
+ : '';
61
+
62
+ return [baseClasses, transitionClasses, borderedClasses, clickableClasses]
63
+ .filter(Boolean)
64
+ .join(' ');
65
+ }
66
+ },
67
+ methods: {
68
+ handleClick(event) {
69
+ if (this.clickable) {
70
+ this.$emit('click', event);
71
+ }
72
+ }
73
+ }
74
+ };
75
+ </script>