@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,97 @@
1
+ <template>
2
+ <div :class="wrapperClasses">
3
+ <div :class="containerClasses" role="status" :aria-label="ariaLabel">
4
+ <svg
5
+ :class="spinnerClasses"
6
+ :style="spinnerStyle"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ fill="none"
9
+ viewBox="0 0 24 24"
10
+ >
11
+ <circle
12
+ class="opacity-25"
13
+ cx="12"
14
+ cy="12"
15
+ r="10"
16
+ stroke="currentColor"
17
+ stroke-width="4"
18
+ />
19
+ <path
20
+ class="opacity-75"
21
+ fill="currentColor"
22
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
23
+ />
24
+ </svg>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script>
30
+ export default {
31
+ name: 'FLoader',
32
+ props: {
33
+ size: {
34
+ type: String,
35
+ default: 'md',
36
+ validator: (value) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(value)
37
+ },
38
+ color: {
39
+ type: String,
40
+ default: ''
41
+ },
42
+ overlay: {
43
+ type: Boolean,
44
+ default: false
45
+ },
46
+ centered: {
47
+ type: Boolean,
48
+ default: false
49
+ },
50
+ label: {
51
+ type: String,
52
+ default: 'Chargement en cours'
53
+ }
54
+ },
55
+ computed: {
56
+ ariaLabel() {
57
+ return this.label;
58
+ },
59
+ wrapperClasses() {
60
+ const overlayClasses = this.overlay
61
+ ? 'fixed inset-0 flex items-center justify-center bg-black/50 z-50'
62
+ : '';
63
+ return [overlayClasses].filter(Boolean).join(' ');
64
+ },
65
+ containerClasses() {
66
+ const baseClasses = 'inline-flex items-center justify-center';
67
+ const centeredClasses =
68
+ this.centered && !this.overlay ? 'absolute inset-0' : '';
69
+
70
+ return [baseClasses, centeredClasses].filter(Boolean).join(' ');
71
+ },
72
+ spinnerClasses() {
73
+ const baseClasses = 'animate-spin';
74
+
75
+ const sizeClasses = {
76
+ xs: 'w-4 h-4',
77
+ sm: 'w-5 h-5',
78
+ md: 'w-6 h-6',
79
+ lg: 'w-8 h-8',
80
+ xl: 'w-12 h-12'
81
+ };
82
+
83
+ const colorClasses = this.color ? '' : 'text-primary-500';
84
+
85
+ return [baseClasses, sizeClasses[this.size], colorClasses]
86
+ .filter(Boolean)
87
+ .join(' ');
88
+ },
89
+ spinnerStyle() {
90
+ if (this.color) {
91
+ return { color: this.color };
92
+ }
93
+ return undefined;
94
+ }
95
+ }
96
+ };
97
+ </script>
@@ -0,0 +1,105 @@
1
+ import FRadio from './FRadio.vue';
2
+
3
+ export default {
4
+ title: 'Atoms/FRadio',
5
+ component: FRadio,
6
+ tags: ['autodocs'],
7
+ argTypes: {
8
+ label: {
9
+ control: 'text',
10
+ description: 'Libellé du bouton radio'
11
+ },
12
+ value: {
13
+ control: 'text',
14
+ description: 'Valeur du bouton radio'
15
+ },
16
+ name: {
17
+ control: 'text',
18
+ description: 'Nom du groupe de boutons radio'
19
+ },
20
+ modelValue: {
21
+ control: 'text',
22
+ description: 'Valeur sélectionnée (v-model)'
23
+ },
24
+ disabled: {
25
+ control: 'boolean',
26
+ description: 'État désactivé'
27
+ },
28
+ error: {
29
+ control: 'boolean',
30
+ description: "État d'erreur"
31
+ }
32
+ }
33
+ };
34
+
35
+ const Template = (args, { argTypes }) => ({
36
+ components: { FRadio },
37
+ props: Object.keys(argTypes),
38
+ data() {
39
+ return { selected: args.modelValue || '' };
40
+ },
41
+ template: '<FRadio v-bind="$props" v-model="selected" />'
42
+ });
43
+
44
+ export const Default = Template.bind({});
45
+ Default.args = {
46
+ label: 'Option 1',
47
+ value: 'option1',
48
+ name: 'default-group'
49
+ };
50
+
51
+ export const RadioGroup = () => ({
52
+ components: { FRadio },
53
+ data() {
54
+ return { selected: 'option1' };
55
+ },
56
+ template: `
57
+ <div class="flex flex-col gap-3">
58
+ <FRadio v-model="selected" name="example" value="option1" label="Option 1" />
59
+ <FRadio v-model="selected" name="example" value="option2" label="Option 2" />
60
+ <FRadio v-model="selected" name="example" value="option3" label="Option 3" />
61
+ <p class="text-sm text-neutral-600 mt-2">Sélectionné: {{ selected }}</p>
62
+ </div>
63
+ `
64
+ });
65
+
66
+ export const Disabled = () => ({
67
+ components: { FRadio },
68
+ data() {
69
+ return { selected: 'option1' };
70
+ },
71
+ template: `
72
+ <div class="flex flex-col gap-3">
73
+ <FRadio v-model="selected" name="disabled-group" value="option1" label="Option sélectionnée désactivée" disabled />
74
+ <FRadio v-model="selected" name="disabled-group" value="option2" label="Option non sélectionnée désactivée" disabled />
75
+ </div>
76
+ `
77
+ });
78
+
79
+ export const WithError = () => ({
80
+ components: { FRadio },
81
+ data() {
82
+ return { selected: '' };
83
+ },
84
+ template: `
85
+ <div class="flex flex-col gap-3">
86
+ <FRadio v-model="selected" name="error-group" value="option1" label="Sélectionnez une option" error />
87
+ <FRadio v-model="selected" name="error-group" value="option2" label="Ou cette option" error />
88
+ <p class="text-xs text-danger-500 mt-1">Veuillez sélectionner une option</p>
89
+ </div>
90
+ `
91
+ });
92
+
93
+ export const Horizontal = () => ({
94
+ components: { FRadio },
95
+ data() {
96
+ return { selected: 'oui' };
97
+ },
98
+ template: `
99
+ <div class="flex gap-6">
100
+ <FRadio v-model="selected" name="horizontal" value="oui" label="Oui" />
101
+ <FRadio v-model="selected" name="horizontal" value="non" label="Non" />
102
+ <FRadio v-model="selected" name="horizontal" value="peut-etre" label="Peut-être" />
103
+ </div>
104
+ `
105
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import FRadio from './FRadio.vue';
4
+
5
+ describe('FRadio', () => {
6
+ it('renders correctly with required props', () => {
7
+ const wrapper = mount(FRadio, {
8
+ propsData: { name: 'test-group', value: 'option1' }
9
+ });
10
+ expect(wrapper.find('input[type="radio"]').exists()).toBe(true);
11
+ });
12
+
13
+ it('displays label when provided', () => {
14
+ const wrapper = mount(FRadio, {
15
+ propsData: { name: 'test', value: 'opt', label: 'Option 1' }
16
+ });
17
+ expect(wrapper.text()).toContain('Option 1');
18
+ });
19
+
20
+ it('sets correct name attribute', () => {
21
+ const wrapper = mount(FRadio, {
22
+ propsData: { name: 'gender', value: 'male' }
23
+ });
24
+ expect(wrapper.find('input').attributes('name')).toBe('gender');
25
+ });
26
+
27
+ it('is unchecked by default', () => {
28
+ const wrapper = mount(FRadio, {
29
+ propsData: { name: 'test', value: 'opt' }
30
+ });
31
+ expect((wrapper.find('input').element as HTMLInputElement).checked).toBe(
32
+ false
33
+ );
34
+ });
35
+
36
+ it('is checked when modelValue matches value', () => {
37
+ const wrapper = mount(FRadio, {
38
+ propsData: { name: 'test', value: 'opt1', modelValue: 'opt1' }
39
+ });
40
+ expect((wrapper.find('input').element as HTMLInputElement).checked).toBe(
41
+ true
42
+ );
43
+ });
44
+
45
+ it('is unchecked when modelValue does not match value', () => {
46
+ const wrapper = mount(FRadio, {
47
+ propsData: { name: 'test', value: 'opt1', modelValue: 'opt2' }
48
+ });
49
+ expect((wrapper.find('input').element as HTMLInputElement).checked).toBe(
50
+ false
51
+ );
52
+ });
53
+
54
+ it('emits change event when selected', async () => {
55
+ const wrapper = mount(FRadio, {
56
+ propsData: { name: 'test', value: 'selected-value' }
57
+ });
58
+ await wrapper.find('input').trigger('change');
59
+ expect(wrapper.emitted('change')).toBeTruthy();
60
+ });
61
+
62
+ it('applies disabled input attribute', () => {
63
+ const wrapper = mount(FRadio, {
64
+ propsData: { name: 'test', value: 'opt', disabled: true }
65
+ });
66
+ expect(wrapper.find('input').attributes('disabled')).toBeDefined();
67
+ });
68
+
69
+ it('applies error styles to radio', () => {
70
+ const wrapper = mount(FRadio, {
71
+ propsData: { name: 'test', value: 'opt', error: true }
72
+ });
73
+ expect(wrapper.html()).toContain('border-danger');
74
+ });
75
+ });
@@ -0,0 +1,119 @@
1
+ <template>
2
+ <label :class="containerClasses">
3
+ <input
4
+ type="radio"
5
+ :class="inputClasses"
6
+ :name="name"
7
+ :value="value"
8
+ :checked="isChecked"
9
+ :disabled="disabled"
10
+ @change="handleChange"
11
+ />
12
+ <span :class="radioClasses" aria-hidden="true">
13
+ <span v-if="isChecked" :class="dotClasses" />
14
+ </span>
15
+ <span v-if="label" :class="labelClasses">
16
+ {{ label }}
17
+ </span>
18
+ </label>
19
+ </template>
20
+
21
+ <script>
22
+ export default {
23
+ name: 'FRadio',
24
+ model: {
25
+ prop: 'modelValue',
26
+ event: 'change'
27
+ },
28
+ props: {
29
+ label: {
30
+ type: String,
31
+ default: ''
32
+ },
33
+ value: {
34
+ type: [String, Number, Boolean],
35
+ required: true
36
+ },
37
+ modelValue: {
38
+ type: [String, Number, Boolean],
39
+ default: null
40
+ },
41
+ name: {
42
+ type: String,
43
+ required: true
44
+ },
45
+ disabled: {
46
+ type: Boolean,
47
+ default: false
48
+ },
49
+ error: {
50
+ type: Boolean,
51
+ default: false
52
+ }
53
+ },
54
+ computed: {
55
+ isChecked() {
56
+ return this.modelValue === this.value;
57
+ },
58
+ containerClasses() {
59
+ const baseClasses = 'inline-flex items-center cursor-pointer font-sans';
60
+ const disabledClasses = this.disabled
61
+ ? 'cursor-not-allowed opacity-70'
62
+ : '';
63
+
64
+ return [baseClasses, disabledClasses].filter(Boolean).join(' ');
65
+ },
66
+ inputClasses() {
67
+ return 'sr-only';
68
+ },
69
+ radioClasses() {
70
+ const baseClasses =
71
+ 'inline-flex items-center justify-center w-5 h-5 rounded-full border-2 flex-shrink-0';
72
+
73
+ const transitionClasses =
74
+ 'transition-all duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]';
75
+
76
+ let stateClasses;
77
+ if (this.error) {
78
+ stateClasses = this.isChecked
79
+ ? 'border-danger-500 bg-danger-500'
80
+ : 'border-danger-500 bg-white';
81
+ } else if (this.disabled) {
82
+ stateClasses = this.isChecked
83
+ ? 'border-neutral-300 bg-neutral-300'
84
+ : 'border-neutral-300 bg-neutral-100';
85
+ } else {
86
+ stateClasses = this.isChecked
87
+ ? 'border-primary-500 bg-primary-500'
88
+ : 'border-neutral-300 bg-white hover:border-primary-400';
89
+ }
90
+
91
+ return [baseClasses, transitionClasses, stateClasses]
92
+ .filter(Boolean)
93
+ .join(' ');
94
+ },
95
+ dotClasses() {
96
+ return 'w-2 h-2 rounded-full bg-white';
97
+ },
98
+ labelClasses() {
99
+ const baseClasses = 'ml-2 text-sm text-neutral-800 select-none';
100
+
101
+ let stateClasses = '';
102
+ if (this.disabled) {
103
+ stateClasses = 'text-neutral-400';
104
+ } else if (this.error) {
105
+ stateClasses = 'text-danger-500';
106
+ }
107
+
108
+ return [baseClasses, stateClasses].filter(Boolean).join(' ');
109
+ }
110
+ },
111
+ methods: {
112
+ handleChange() {
113
+ if (!this.disabled) {
114
+ this.$emit('change', this.value);
115
+ }
116
+ }
117
+ }
118
+ };
119
+ </script>
@@ -0,0 +1,126 @@
1
+ import FTextarea from './FTextarea.vue';
2
+
3
+ export default {
4
+ title: 'Atoms/FTextarea',
5
+ component: FTextarea,
6
+ tags: ['autodocs'],
7
+ argTypes: {
8
+ value: {
9
+ control: 'text',
10
+ description: 'Valeur du champ'
11
+ },
12
+ label: {
13
+ control: 'text',
14
+ description: 'Libellé du champ'
15
+ },
16
+ placeholder: {
17
+ control: 'text',
18
+ description: 'Texte de placeholder'
19
+ },
20
+ rows: {
21
+ control: { type: 'number', min: 1, max: 20 },
22
+ description: 'Nombre de lignes'
23
+ },
24
+ disabled: {
25
+ control: 'boolean',
26
+ description: 'État désactivé'
27
+ },
28
+ readonly: {
29
+ control: 'boolean',
30
+ description: 'Lecture seule'
31
+ },
32
+ error: {
33
+ control: 'boolean',
34
+ description: "État d'erreur"
35
+ },
36
+ errorMessage: {
37
+ control: 'text',
38
+ description: "Message d'erreur"
39
+ },
40
+ maxlength: {
41
+ control: 'number',
42
+ description: 'Longueur maximale'
43
+ },
44
+ showCounter: {
45
+ control: 'boolean',
46
+ description: 'Afficher le compteur de caractères'
47
+ }
48
+ }
49
+ };
50
+
51
+ const Template = (args, { argTypes }) => ({
52
+ components: { FTextarea },
53
+ props: Object.keys(argTypes),
54
+ data() {
55
+ return { content: args.value || '' };
56
+ },
57
+ template: '<FTextarea v-bind="$props" v-model="content" />'
58
+ });
59
+
60
+ export const Default = Template.bind({});
61
+ Default.args = {
62
+ placeholder: 'Saisissez votre texte...'
63
+ };
64
+
65
+ export const WithLabel = Template.bind({});
66
+ WithLabel.args = {
67
+ label: 'Description',
68
+ placeholder: 'Décrivez votre projet...'
69
+ };
70
+
71
+ export const WithValue = Template.bind({});
72
+ WithValue.args = {
73
+ label: 'Commentaire',
74
+ value: 'Ceci est un exemple de texte saisi dans le champ textarea.'
75
+ };
76
+
77
+ export const CustomRows = Template.bind({});
78
+ CustomRows.args = {
79
+ label: 'Long texte',
80
+ rows: 6,
81
+ placeholder: 'Saisissez un texte plus long...'
82
+ };
83
+
84
+ export const WithCounter = Template.bind({});
85
+ WithCounter.args = {
86
+ label: 'Bio',
87
+ placeholder: 'Présentez-vous...',
88
+ maxlength: 200,
89
+ showCounter: true
90
+ };
91
+
92
+ export const Disabled = Template.bind({});
93
+ Disabled.args = {
94
+ label: 'Champ désactivé',
95
+ disabled: true,
96
+ value: 'Ce champ est désactivé'
97
+ };
98
+
99
+ export const Readonly = Template.bind({});
100
+ Readonly.args = {
101
+ label: 'Lecture seule',
102
+ readonly: true,
103
+ value: 'Ce texte ne peut pas être modifié'
104
+ };
105
+
106
+ export const WithError = Template.bind({});
107
+ WithError.args = {
108
+ label: 'Description',
109
+ error: true,
110
+ errorMessage: 'Ce champ est obligatoire'
111
+ };
112
+
113
+ export const States = () => ({
114
+ components: { FTextarea },
115
+ data() {
116
+ return { val1: '', val2: '', val3: '', val4: '' };
117
+ },
118
+ template: `
119
+ <div class="flex flex-col gap-4">
120
+ <FTextarea v-model="val1" label="Normal" placeholder="Saisissez du texte..." />
121
+ <FTextarea v-model="val2" label="Désactivé" placeholder="Désactivé" disabled />
122
+ <FTextarea v-model="val3" label="Lecture seule" value="En lecture seule" readonly />
123
+ <FTextarea v-model="val4" label="Avec erreur" placeholder="Champ avec erreur" error errorMessage="Ce champ est invalide" />
124
+ </div>
125
+ `
126
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import FTextarea from './FTextarea.vue';
4
+
5
+ describe('FTextarea', () => {
6
+ it('renders correctly with default props', () => {
7
+ const wrapper = mount(FTextarea);
8
+ expect(wrapper.find('textarea').exists()).toBe(true);
9
+ });
10
+
11
+ it('displays label when provided', () => {
12
+ const wrapper = mount(FTextarea, {
13
+ propsData: { label: 'Description' }
14
+ });
15
+ expect(wrapper.text()).toContain('Description');
16
+ });
17
+
18
+ it('displays placeholder', () => {
19
+ const wrapper = mount(FTextarea, {
20
+ propsData: { placeholder: 'Enter text...' }
21
+ });
22
+ expect(wrapper.find('textarea').attributes('placeholder')).toBe(
23
+ 'Enter text...'
24
+ );
25
+ });
26
+
27
+ it('displays value', () => {
28
+ const wrapper = mount(FTextarea, {
29
+ propsData: { value: 'Test content' }
30
+ });
31
+ expect(
32
+ (wrapper.find('textarea').element as HTMLTextAreaElement).value
33
+ ).toBe('Test content');
34
+ });
35
+
36
+ it('emits input event when typing', async () => {
37
+ const wrapper = mount(FTextarea);
38
+ await wrapper.find('textarea').setValue('New content');
39
+ expect(wrapper.emitted('input')).toBeTruthy();
40
+ expect(wrapper.emitted('input')![0]).toEqual(['New content']);
41
+ });
42
+
43
+ it('sets correct rows attribute', () => {
44
+ const wrapper = mount(FTextarea, {
45
+ propsData: { rows: 5 }
46
+ });
47
+ expect(wrapper.find('textarea').attributes('rows')).toBe('5');
48
+ });
49
+
50
+ it('applies disabled state', () => {
51
+ const wrapper = mount(FTextarea, {
52
+ propsData: { disabled: true }
53
+ });
54
+ expect(wrapper.find('textarea').attributes('disabled')).toBeDefined();
55
+ expect(wrapper.find('textarea').classes()).toContain('cursor-not-allowed');
56
+ });
57
+
58
+ it('applies readonly state', () => {
59
+ const wrapper = mount(FTextarea, {
60
+ propsData: { readonly: true }
61
+ });
62
+ expect(wrapper.find('textarea').attributes('readonly')).toBeDefined();
63
+ });
64
+
65
+ it('applies error styles', () => {
66
+ const wrapper = mount(FTextarea, {
67
+ propsData: { error: true }
68
+ });
69
+ expect(wrapper.find('textarea').classes().join(' ')).toContain(
70
+ 'border-danger'
71
+ );
72
+ });
73
+
74
+ it('displays error message', () => {
75
+ const wrapper = mount(FTextarea, {
76
+ propsData: { errorMessage: 'This field is required' }
77
+ });
78
+ expect(wrapper.text()).toContain('This field is required');
79
+ });
80
+
81
+ it('displays character counter when showCounter and maxlength are set', () => {
82
+ const wrapper = mount(FTextarea, {
83
+ propsData: { showCounter: true, maxlength: 100, value: 'Hello' }
84
+ });
85
+ expect(wrapper.text()).toContain('5/100');
86
+ });
87
+
88
+ it('sets maxlength attribute', () => {
89
+ const wrapper = mount(FTextarea, {
90
+ propsData: { maxlength: 200 }
91
+ });
92
+ expect(wrapper.find('textarea').attributes('maxlength')).toBe('200');
93
+ });
94
+ });