@pyreweb/fabric 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/fabric.cjs.js +18109 -0
- package/dist/fabric.css +2180 -0
- package/dist/fabric.esm.js +18062 -0
- package/dist/fabric.min.js +18112 -0
- package/dist/types/components/atoms/FAvatar/FAvatar.test.d.ts +1 -0
- package/dist/types/components/atoms/FBadge/FBadge.test.d.ts +1 -0
- package/dist/types/components/atoms/FButton/FButton.test.d.ts +1 -0
- package/dist/types/components/atoms/FCheckbox/FCheckbox.test.d.ts +1 -0
- package/dist/types/components/atoms/FDivider/FDivider.test.d.ts +1 -0
- package/dist/types/components/atoms/FIcon/FIcon.test.d.ts +1 -0
- package/dist/types/components/atoms/FInput/FInput.test.d.ts +1 -0
- package/dist/types/components/atoms/FLoader/FLoader.test.d.ts +1 -0
- package/dist/types/components/atoms/FRadio/FRadio.test.d.ts +1 -0
- package/dist/types/components/atoms/FTextarea/FTextarea.test.d.ts +1 -0
- package/dist/types/components/atoms/FToggle/FToggle.test.d.ts +1 -0
- package/dist/types/components/atoms/FTypography/FTypography.test.d.ts +1 -0
- package/dist/types/components/atoms/index.d.ts +13 -0
- package/dist/types/components/molecules/FAccordionItem/FAccordionItem.test.d.ts +1 -0
- package/dist/types/components/molecules/FAlert/FAlert.test.d.ts +1 -0
- package/dist/types/components/molecules/FBreadcrumb/FBreadcrumb.test.d.ts +1 -0
- package/dist/types/components/molecules/FButtonGroup/FButtonGroup.test.d.ts +1 -0
- package/dist/types/components/molecules/FCard/FCard.test.d.ts +1 -0
- package/dist/types/components/molecules/FDatePicker/FDatePicker.test.d.ts +1 -0
- package/dist/types/components/molecules/FEmptyState/FEmptyState.test.d.ts +1 -0
- package/dist/types/components/molecules/FFilePreview/FFilePreview.test.d.ts +1 -0
- package/dist/types/components/molecules/FFormField/FFormField.test.d.ts +1 -0
- package/dist/types/components/molecules/FListItem/FListItem.test.d.ts +1 -0
- package/dist/types/components/molecules/FPagination/FPagination.test.d.ts +1 -0
- package/dist/types/components/molecules/FSearchBar/FSearchBar.test.d.ts +1 -0
- package/dist/types/components/molecules/FSelect/FSelect.test.d.ts +1 -0
- package/dist/types/components/molecules/FStatCard/FStatCard.test.d.ts +1 -0
- package/dist/types/components/molecules/FTabs/FTabs.test.d.ts +1 -0
- package/dist/types/components/molecules/FToast/FToast.test.d.ts +1 -0
- package/dist/types/components/molecules/index.d.ts +18 -0
- package/dist/types/components/organisms/FActivityFeed/FActivityFeed.test.d.ts +1 -0
- package/dist/types/components/organisms/FDataTable/FDataTable.test.d.ts +1 -0
- package/dist/types/components/organisms/FDrawer/FDrawer.test.d.ts +1 -0
- package/dist/types/components/organisms/FFileUpload/FFileUpload.test.d.ts +1 -0
- package/dist/types/components/organisms/FFilterSidebar/FFilterSidebar.test.d.ts +1 -0
- package/dist/types/components/organisms/FForm/FForm.test.d.ts +1 -0
- package/dist/types/components/organisms/FModal/FModal.test.d.ts +1 -0
- package/dist/types/components/organisms/FNavigationSidebar/FNavigationSidebar.test.d.ts +1 -0
- package/dist/types/components/organisms/FOnboardingStepper/FOnboardingStepper.test.d.ts +1 -0
- package/dist/types/components/organisms/FOnboardingStepper/FStepperProgress.test.d.ts +1 -0
- package/dist/types/components/organisms/FPageHeader/FPageHeader.test.d.ts +1 -0
- package/dist/types/components/organisms/FProfileSection/FProfileSection.test.d.ts +1 -0
- package/dist/types/components/organisms/FToastProvider/FToastProvider.test.d.ts +1 -0
- package/dist/types/components/organisms/FUserMenu/FUserMenu.test.d.ts +1 -0
- package/dist/types/components/organisms/index.d.ts +14 -0
- package/dist/types/components/utils/FThemeProvider.test.d.ts +1 -0
- package/dist/types/components/utils/index.d.ts +2 -0
- package/dist/types/components.d.ts +602 -0
- package/dist/types/composables/index.d.ts +12 -0
- package/dist/types/composables/useDataTableState.d.ts +106 -0
- package/dist/types/composables/useDataTableState.test.d.ts +1 -0
- package/dist/types/composables/useFormValidation.d.ts +49 -0
- package/dist/types/composables/useFormValidation.test.d.ts +1 -0
- package/dist/types/composables/useSidebarState.d.ts +65 -0
- package/dist/types/composables/useSidebarState.test.d.ts +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/types.d.ts +529 -0
- package/package.json +100 -0
- package/src/components/atoms/FAvatar/FAvatar.stories.js +100 -0
- package/src/components/atoms/FAvatar/FAvatar.test.ts +95 -0
- package/src/components/atoms/FAvatar/FAvatar.vue +190 -0
- package/src/components/atoms/FBadge/FBadge.stories.js +129 -0
- package/src/components/atoms/FBadge/FBadge.test.ts +93 -0
- package/src/components/atoms/FBadge/FBadge.vue +103 -0
- package/src/components/atoms/FButton/FButton.stories.js +122 -0
- package/src/components/atoms/FButton/FButton.test.ts +98 -0
- package/src/components/atoms/FButton/FButton.vue +147 -0
- package/src/components/atoms/FCheckbox/FCheckbox.stories.js +96 -0
- package/src/components/atoms/FCheckbox/FCheckbox.test.ts +64 -0
- package/src/components/atoms/FCheckbox/FCheckbox.vue +76 -0
- package/src/components/atoms/FDivider/FDivider.stories.js +104 -0
- package/src/components/atoms/FDivider/FDivider.test.ts +80 -0
- package/src/components/atoms/FDivider/FDivider.vue +117 -0
- package/src/components/atoms/FIcon/FIcon.stories.js +189 -0
- package/src/components/atoms/FIcon/FIcon.test.ts +99 -0
- package/src/components/atoms/FIcon/FIcon.vue +192 -0
- package/src/components/atoms/FInput/FInput.stories.js +119 -0
- package/src/components/atoms/FInput/FInput.test.ts +79 -0
- package/src/components/atoms/FInput/FInput.vue +88 -0
- package/src/components/atoms/FLoader/FLoader.stories.js +109 -0
- package/src/components/atoms/FLoader/FLoader.test.ts +66 -0
- package/src/components/atoms/FLoader/FLoader.vue +97 -0
- package/src/components/atoms/FRadio/FRadio.stories.js +105 -0
- package/src/components/atoms/FRadio/FRadio.test.ts +75 -0
- package/src/components/atoms/FRadio/FRadio.vue +119 -0
- package/src/components/atoms/FTextarea/FTextarea.stories.js +126 -0
- package/src/components/atoms/FTextarea/FTextarea.test.ts +94 -0
- package/src/components/atoms/FTextarea/FTextarea.vue +156 -0
- package/src/components/atoms/FToggle/FToggle.stories.js +108 -0
- package/src/components/atoms/FToggle/FToggle.test.ts +96 -0
- package/src/components/atoms/FToggle/FToggle.vue +123 -0
- package/src/components/atoms/FTypography/FTypography.stories.js +127 -0
- package/src/components/atoms/FTypography/FTypography.test.ts +93 -0
- package/src/components/atoms/FTypography/FTypography.vue +78 -0
- package/src/components/atoms/index.ts +27 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.stories.js +71 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.test.ts +61 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.vue +105 -0
- package/src/components/molecules/FAlert/FAlert.stories.js +87 -0
- package/src/components/molecules/FAlert/FAlert.test.ts +59 -0
- package/src/components/molecules/FAlert/FAlert.vue +108 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.stories.js +90 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.test.ts +76 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.vue +117 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.stories.js +82 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.test.ts +44 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.vue +31 -0
- package/src/components/molecules/FCard/FCard.stories.js +136 -0
- package/src/components/molecules/FCard/FCard.test.ts +87 -0
- package/src/components/molecules/FCard/FCard.vue +75 -0
- package/src/components/molecules/FDatePicker/FDatePicker.stories.js +305 -0
- package/src/components/molecules/FDatePicker/FDatePicker.test.ts +282 -0
- package/src/components/molecules/FDatePicker/FDatePicker.vue +750 -0
- package/src/components/molecules/FEmptyState/FEmptyState.stories.js +98 -0
- package/src/components/molecules/FEmptyState/FEmptyState.test.ts +82 -0
- package/src/components/molecules/FEmptyState/FEmptyState.vue +89 -0
- package/src/components/molecules/FFilePreview/FFilePreview.stories.js +130 -0
- package/src/components/molecules/FFilePreview/FFilePreview.test.ts +70 -0
- package/src/components/molecules/FFilePreview/FFilePreview.vue +125 -0
- package/src/components/molecules/FFormField/FFormField.stories.js +149 -0
- package/src/components/molecules/FFormField/FFormField.test.ts +85 -0
- package/src/components/molecules/FFormField/FFormField.vue +107 -0
- package/src/components/molecules/FListItem/FListItem.stories.js +158 -0
- package/src/components/molecules/FListItem/FListItem.test.ts +93 -0
- package/src/components/molecules/FListItem/FListItem.vue +113 -0
- package/src/components/molecules/FPagination/FPagination.stories.js +132 -0
- package/src/components/molecules/FPagination/FPagination.test.ts +79 -0
- package/src/components/molecules/FPagination/FPagination.vue +206 -0
- package/src/components/molecules/FSearchBar/FSearchBar.stories.js +129 -0
- package/src/components/molecules/FSearchBar/FSearchBar.test.ts +81 -0
- package/src/components/molecules/FSearchBar/FSearchBar.vue +180 -0
- package/src/components/molecules/FSelect/FSelect.stories.js +333 -0
- package/src/components/molecules/FSelect/FSelect.test.ts +478 -0
- package/src/components/molecules/FSelect/FSelect.vue +551 -0
- package/src/components/molecules/FStatCard/FStatCard.stories.js +144 -0
- package/src/components/molecules/FStatCard/FStatCard.test.ts +78 -0
- package/src/components/molecules/FStatCard/FStatCard.vue +106 -0
- package/src/components/molecules/FTabs/FTab.vue +63 -0
- package/src/components/molecules/FTabs/FTabs.stories.js +277 -0
- package/src/components/molecules/FTabs/FTabs.test.ts +264 -0
- package/src/components/molecules/FTabs/FTabs.vue +273 -0
- package/src/components/molecules/FToast/FToast.stories.js +150 -0
- package/src/components/molecules/FToast/FToast.test.ts +157 -0
- package/src/components/molecules/FToast/FToast.vue +283 -0
- package/src/components/molecules/index.ts +37 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.stories.js +217 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.test.ts +134 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.vue +589 -0
- package/src/components/organisms/FDataTable/FDataTable.stories.js +370 -0
- package/src/components/organisms/FDataTable/FDataTable.test.ts +248 -0
- package/src/components/organisms/FDataTable/FDataTable.vue +808 -0
- package/src/components/organisms/FDrawer/FDrawer.stories.js +296 -0
- package/src/components/organisms/FDrawer/FDrawer.test.ts +142 -0
- package/src/components/organisms/FDrawer/FDrawer.vue +303 -0
- package/src/components/organisms/FFileUpload/FFileUpload.stories.js +162 -0
- package/src/components/organisms/FFileUpload/FFileUpload.test.ts +103 -0
- package/src/components/organisms/FFileUpload/FFileUpload.vue +616 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.stories.js +161 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.test.ts +92 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.vue +458 -0
- package/src/components/organisms/FForm/FForm.stories.js +270 -0
- package/src/components/organisms/FForm/FForm.test.ts +63 -0
- package/src/components/organisms/FForm/FForm.vue +19 -0
- package/src/components/organisms/FModal/FModal.stories.js +227 -0
- package/src/components/organisms/FModal/FModal.test.ts +181 -0
- package/src/components/organisms/FModal/FModal.vue +319 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.stories.js +176 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.test.ts +95 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.vue +577 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.stories.js +197 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.test.ts +114 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.vue +212 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.stories.js +122 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.test.ts +130 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.vue +146 -0
- package/src/components/organisms/FPageHeader/FPageHeader.stories.js +142 -0
- package/src/components/organisms/FPageHeader/FPageHeader.test.ts +83 -0
- package/src/components/organisms/FPageHeader/FPageHeader.vue +241 -0
- package/src/components/organisms/FProfileSection/FProfileSection.stories.js +190 -0
- package/src/components/organisms/FProfileSection/FProfileSection.test.ts +85 -0
- package/src/components/organisms/FProfileSection/FProfileSection.vue +562 -0
- package/src/components/organisms/FToastProvider/FToastProvider.stories.js +290 -0
- package/src/components/organisms/FToastProvider/FToastProvider.test.ts +215 -0
- package/src/components/organisms/FToastProvider/FToastProvider.vue +214 -0
- package/src/components/organisms/FUserMenu/FUserMenu.stories.js +170 -0
- package/src/components/organisms/FUserMenu/FUserMenu.test.ts +102 -0
- package/src/components/organisms/FUserMenu/FUserMenu.vue +407 -0
- package/src/components/organisms/index.ts +29 -0
- package/src/components/utils/FThemeProvider.stories.js +236 -0
- package/src/components/utils/FThemeProvider.test.ts +244 -0
- package/src/components/utils/FThemeProvider.vue +191 -0
- package/src/components/utils/index.ts +3 -0
- package/src/components.d.ts +602 -0
- package/src/composables/README.md +233 -0
- package/src/composables/index.ts +25 -0
- package/src/composables/useDataTableState.test.ts +378 -0
- package/src/composables/useDataTableState.ts +361 -0
- package/src/composables/useFormValidation.test.ts +198 -0
- package/src/composables/useFormValidation.ts +178 -0
- package/src/composables/useSidebarState.test.ts +307 -0
- package/src/composables/useSidebarState.ts +201 -0
- package/src/env.d.ts +14 -0
- package/src/index.ts +167 -0
- package/src/styles/tailwind.css +173 -0
- package/src/types.ts +740 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import FFormField from './FFormField.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Molecules/FFormField',
|
|
5
|
+
component: FFormField,
|
|
6
|
+
tags: ['autodocs'],
|
|
7
|
+
argTypes: {
|
|
8
|
+
label: {
|
|
9
|
+
control: 'text',
|
|
10
|
+
description: 'Libellé du champ'
|
|
11
|
+
},
|
|
12
|
+
value: {
|
|
13
|
+
control: 'text',
|
|
14
|
+
description: 'Valeur du champ'
|
|
15
|
+
},
|
|
16
|
+
type: {
|
|
17
|
+
control: { type: 'select' },
|
|
18
|
+
options: ['text', 'email', 'password', 'number'],
|
|
19
|
+
description: 'Type du champ'
|
|
20
|
+
},
|
|
21
|
+
placeholder: {
|
|
22
|
+
control: 'text',
|
|
23
|
+
description: 'Placeholder'
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
control: { type: 'select' },
|
|
27
|
+
options: ['small', 'medium', 'large'],
|
|
28
|
+
description: 'Taille du champ'
|
|
29
|
+
},
|
|
30
|
+
disabled: {
|
|
31
|
+
control: 'boolean',
|
|
32
|
+
description: 'État désactivé'
|
|
33
|
+
},
|
|
34
|
+
readonly: {
|
|
35
|
+
control: 'boolean',
|
|
36
|
+
description: 'Lecture seule'
|
|
37
|
+
},
|
|
38
|
+
required: {
|
|
39
|
+
control: 'boolean',
|
|
40
|
+
description: 'Champ obligatoire'
|
|
41
|
+
},
|
|
42
|
+
hint: {
|
|
43
|
+
control: 'text',
|
|
44
|
+
description: "Texte d'aide"
|
|
45
|
+
},
|
|
46
|
+
errorMessage: {
|
|
47
|
+
control: 'text',
|
|
48
|
+
description: "Message d'erreur"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const Template = (args, { argTypes }) => ({
|
|
54
|
+
components: { FFormField },
|
|
55
|
+
props: Object.keys(argTypes),
|
|
56
|
+
data() {
|
|
57
|
+
return { inputValue: args.value || '' };
|
|
58
|
+
},
|
|
59
|
+
template: '<FFormField v-bind="$props" v-model="inputValue" />'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const Default = Template.bind({});
|
|
63
|
+
Default.args = {
|
|
64
|
+
label: 'Adresse email',
|
|
65
|
+
placeholder: 'exemple@email.com'
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const WithHint = Template.bind({});
|
|
69
|
+
WithHint.args = {
|
|
70
|
+
label: 'Mot de passe',
|
|
71
|
+
type: 'password',
|
|
72
|
+
hint: 'Le mot de passe doit contenir au moins 8 caractères'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const Required = Template.bind({});
|
|
76
|
+
Required.args = {
|
|
77
|
+
label: 'Nom complet',
|
|
78
|
+
placeholder: 'Jean Dupont',
|
|
79
|
+
required: true
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const WithError = Template.bind({});
|
|
83
|
+
WithError.args = {
|
|
84
|
+
label: 'Email',
|
|
85
|
+
value: 'email-invalide',
|
|
86
|
+
errorMessage: 'Veuillez entrer une adresse email valide'
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const Disabled = Template.bind({});
|
|
90
|
+
Disabled.args = {
|
|
91
|
+
label: 'Champ désactivé',
|
|
92
|
+
value: 'Valeur non modifiable',
|
|
93
|
+
disabled: true
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const Readonly = Template.bind({});
|
|
97
|
+
Readonly.args = {
|
|
98
|
+
label: 'Référence',
|
|
99
|
+
value: 'REF-2024-001',
|
|
100
|
+
readonly: true
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const Sizes = () => ({
|
|
104
|
+
components: { FFormField },
|
|
105
|
+
template: `
|
|
106
|
+
<div class="flex flex-col gap-4">
|
|
107
|
+
<FFormField label="Petit" size="small" placeholder="Petit champ" />
|
|
108
|
+
<FFormField label="Moyen" size="medium" placeholder="Champ moyen" />
|
|
109
|
+
<FFormField label="Grand" size="large" placeholder="Grand champ" />
|
|
110
|
+
</div>
|
|
111
|
+
`
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const CompleteForm = () => ({
|
|
115
|
+
components: { FFormField },
|
|
116
|
+
data() {
|
|
117
|
+
return {
|
|
118
|
+
form: {
|
|
119
|
+
name: '',
|
|
120
|
+
email: '',
|
|
121
|
+
password: ''
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
template: `
|
|
126
|
+
<div class="flex flex-col gap-4 max-w-md">
|
|
127
|
+
<FFormField
|
|
128
|
+
v-model="form.name"
|
|
129
|
+
label="Nom complet"
|
|
130
|
+
placeholder="Votre nom"
|
|
131
|
+
required
|
|
132
|
+
/>
|
|
133
|
+
<FFormField
|
|
134
|
+
v-model="form.email"
|
|
135
|
+
label="Email"
|
|
136
|
+
type="email"
|
|
137
|
+
placeholder="exemple@email.com"
|
|
138
|
+
required
|
|
139
|
+
/>
|
|
140
|
+
<FFormField
|
|
141
|
+
v-model="form.password"
|
|
142
|
+
label="Mot de passe"
|
|
143
|
+
type="password"
|
|
144
|
+
hint="8 caractères minimum"
|
|
145
|
+
required
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
`
|
|
149
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import FFormField from './FFormField.vue';
|
|
4
|
+
|
|
5
|
+
describe('FFormField', () => {
|
|
6
|
+
it('renders correctly with default props', () => {
|
|
7
|
+
const wrapper = mount(FFormField);
|
|
8
|
+
expect(wrapper.findComponent({ name: 'FInput' }).exists()).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('displays label when provided', () => {
|
|
12
|
+
const wrapper = mount(FFormField, {
|
|
13
|
+
propsData: { label: 'Email Address' }
|
|
14
|
+
});
|
|
15
|
+
expect(wrapper.text()).toContain('Email Address');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('displays hint when provided', () => {
|
|
19
|
+
const wrapper = mount(FFormField, {
|
|
20
|
+
propsData: { hint: 'Enter a valid email' }
|
|
21
|
+
});
|
|
22
|
+
expect(wrapper.text()).toContain('Enter a valid email');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('displays error message when provided', () => {
|
|
26
|
+
const wrapper = mount(FFormField, {
|
|
27
|
+
propsData: { errorMessage: 'This field is required' }
|
|
28
|
+
});
|
|
29
|
+
expect(wrapper.text()).toContain('This field is required');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('shows required indicator when required', () => {
|
|
33
|
+
const wrapper = mount(FFormField, {
|
|
34
|
+
propsData: { label: 'Name', required: true }
|
|
35
|
+
});
|
|
36
|
+
expect(wrapper.html()).toContain('*');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('passes value to input', () => {
|
|
40
|
+
const wrapper = mount(FFormField, {
|
|
41
|
+
propsData: { value: 'test@example.com' }
|
|
42
|
+
});
|
|
43
|
+
const input = wrapper.findComponent({ name: 'FInput' });
|
|
44
|
+
expect(input.props('value')).toBe('test@example.com');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('emits input event', async () => {
|
|
48
|
+
const wrapper = mount(FFormField);
|
|
49
|
+
const input = wrapper.findComponent({ name: 'FInput' });
|
|
50
|
+
await input.vm.$emit('input', 'new value');
|
|
51
|
+
expect(wrapper.emitted('input')).toBeTruthy();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('passes placeholder to input', () => {
|
|
55
|
+
const wrapper = mount(FFormField, {
|
|
56
|
+
propsData: { placeholder: 'Enter your email' }
|
|
57
|
+
});
|
|
58
|
+
const input = wrapper.findComponent({ name: 'FInput' });
|
|
59
|
+
expect(input.props('placeholder')).toBe('Enter your email');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('passes size to input', () => {
|
|
63
|
+
const wrapper = mount(FFormField, {
|
|
64
|
+
propsData: { size: 'large' }
|
|
65
|
+
});
|
|
66
|
+
const input = wrapper.findComponent({ name: 'FInput' });
|
|
67
|
+
expect(input.props('size')).toBe('large');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('passes disabled state to input', () => {
|
|
71
|
+
const wrapper = mount(FFormField, {
|
|
72
|
+
propsData: { disabled: true }
|
|
73
|
+
});
|
|
74
|
+
const input = wrapper.findComponent({ name: 'FInput' });
|
|
75
|
+
expect(input.props('disabled')).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('passes error state based on errorMessage', () => {
|
|
79
|
+
const wrapper = mount(FFormField, {
|
|
80
|
+
propsData: { errorMessage: 'Error!' }
|
|
81
|
+
});
|
|
82
|
+
const input = wrapper.findComponent({ name: 'FInput' });
|
|
83
|
+
expect(input.props('error')).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col gap-1.5">
|
|
3
|
+
<label
|
|
4
|
+
v-if="label"
|
|
5
|
+
:for="inputId"
|
|
6
|
+
:class="[
|
|
7
|
+
'text-sm font-medium text-neutral-700',
|
|
8
|
+
{ 'after:content-[\'_*\'] after:text-danger-500': required }
|
|
9
|
+
]"
|
|
10
|
+
>
|
|
11
|
+
{{ label }}
|
|
12
|
+
</label>
|
|
13
|
+
<f-input
|
|
14
|
+
:id="inputId"
|
|
15
|
+
:value="value"
|
|
16
|
+
:type="type"
|
|
17
|
+
:placeholder="placeholder"
|
|
18
|
+
:size="size"
|
|
19
|
+
:disabled="disabled"
|
|
20
|
+
:readonly="readonly"
|
|
21
|
+
:error="!!errorMessage"
|
|
22
|
+
@input="$emit('input', $event)"
|
|
23
|
+
@focus="$emit('focus', $event)"
|
|
24
|
+
@blur="$emit('blur', $event)"
|
|
25
|
+
/>
|
|
26
|
+
<span v-if="errorMessage" class="text-xs text-danger-500">
|
|
27
|
+
{{ errorMessage }}
|
|
28
|
+
</span>
|
|
29
|
+
<span v-else-if="hint" class="text-xs text-neutral-500">
|
|
30
|
+
{{ hint }}
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
import FInput from '../../atoms/FInput/FInput.vue';
|
|
37
|
+
|
|
38
|
+
let idCounter = 0;
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
name: 'FFormField',
|
|
42
|
+
components: {
|
|
43
|
+
FInput
|
|
44
|
+
},
|
|
45
|
+
props: {
|
|
46
|
+
id: {
|
|
47
|
+
type: String,
|
|
48
|
+
default: ''
|
|
49
|
+
},
|
|
50
|
+
label: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: ''
|
|
53
|
+
},
|
|
54
|
+
value: {
|
|
55
|
+
type: [String, Number],
|
|
56
|
+
default: ''
|
|
57
|
+
},
|
|
58
|
+
type: {
|
|
59
|
+
type: String,
|
|
60
|
+
default: 'text'
|
|
61
|
+
},
|
|
62
|
+
placeholder: {
|
|
63
|
+
type: String,
|
|
64
|
+
default: ''
|
|
65
|
+
},
|
|
66
|
+
size: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: 'medium'
|
|
69
|
+
},
|
|
70
|
+
disabled: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
default: false
|
|
73
|
+
},
|
|
74
|
+
readonly: {
|
|
75
|
+
type: Boolean,
|
|
76
|
+
default: false
|
|
77
|
+
},
|
|
78
|
+
required: {
|
|
79
|
+
type: Boolean,
|
|
80
|
+
default: false
|
|
81
|
+
},
|
|
82
|
+
hint: {
|
|
83
|
+
type: String,
|
|
84
|
+
default: ''
|
|
85
|
+
},
|
|
86
|
+
errorMessage: {
|
|
87
|
+
type: String,
|
|
88
|
+
default: ''
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
data() {
|
|
92
|
+
return {
|
|
93
|
+
generatedId: ''
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
computed: {
|
|
97
|
+
inputId() {
|
|
98
|
+
return this.id || this.generatedId;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
created() {
|
|
102
|
+
if (!this.id) {
|
|
103
|
+
this.generatedId = `f-form-field-${++idCounter}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
</script>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import FListItem from './FListItem.vue';
|
|
2
|
+
import FAvatar from '../../atoms/FAvatar/FAvatar.vue';
|
|
3
|
+
import FBadge from '../../atoms/FBadge/FBadge.vue';
|
|
4
|
+
import FIcon from '../../atoms/FIcon/FIcon.vue';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Molecules/FListItem',
|
|
8
|
+
component: FListItem,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
title: {
|
|
12
|
+
control: 'text',
|
|
13
|
+
description: 'Titre principal'
|
|
14
|
+
},
|
|
15
|
+
subtitle: {
|
|
16
|
+
control: 'text',
|
|
17
|
+
description: 'Sous-titre'
|
|
18
|
+
},
|
|
19
|
+
clickable: {
|
|
20
|
+
control: 'boolean',
|
|
21
|
+
description: 'Élément cliquable'
|
|
22
|
+
},
|
|
23
|
+
selected: {
|
|
24
|
+
control: 'boolean',
|
|
25
|
+
description: 'Élément sélectionné'
|
|
26
|
+
},
|
|
27
|
+
disabled: {
|
|
28
|
+
control: 'boolean',
|
|
29
|
+
description: 'État désactivé'
|
|
30
|
+
},
|
|
31
|
+
truncate: {
|
|
32
|
+
control: 'boolean',
|
|
33
|
+
description: 'Tronquer le texte'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const Template = (args, { argTypes }) => ({
|
|
39
|
+
components: { FListItem },
|
|
40
|
+
props: Object.keys(argTypes),
|
|
41
|
+
template: '<FListItem v-bind="$props" />'
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const Default = Template.bind({});
|
|
45
|
+
Default.args = {
|
|
46
|
+
title: "Titre de l'élément",
|
|
47
|
+
subtitle: 'Description secondaire'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Clickable = Template.bind({});
|
|
51
|
+
Clickable.args = {
|
|
52
|
+
title: 'Élément cliquable',
|
|
53
|
+
subtitle: 'Cliquez pour interagir',
|
|
54
|
+
clickable: true
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const Selected = Template.bind({});
|
|
58
|
+
Selected.args = {
|
|
59
|
+
title: 'Élément sélectionné',
|
|
60
|
+
subtitle: 'Cet élément est actif',
|
|
61
|
+
selected: true
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Disabled = Template.bind({});
|
|
65
|
+
Disabled.args = {
|
|
66
|
+
title: 'Élément désactivé',
|
|
67
|
+
subtitle: 'Non interactif',
|
|
68
|
+
disabled: true
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const WithAvatar = () => ({
|
|
72
|
+
components: { FListItem, FAvatar },
|
|
73
|
+
template: `
|
|
74
|
+
<FListItem title="Jean Dupont" subtitle="Développeur" clickable>
|
|
75
|
+
<template #left>
|
|
76
|
+
<FAvatar name="Jean Dupont" />
|
|
77
|
+
</template>
|
|
78
|
+
</FListItem>
|
|
79
|
+
`
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const WithIcon = () => ({
|
|
83
|
+
components: { FListItem, FIcon },
|
|
84
|
+
template: `
|
|
85
|
+
<FListItem title="Paramètres" subtitle="Gérer vos préférences" clickable>
|
|
86
|
+
<template #left>
|
|
87
|
+
<FIcon name="cog" size="md" class="text-neutral-500" />
|
|
88
|
+
</template>
|
|
89
|
+
<template #right>
|
|
90
|
+
<FIcon name="chevron-right" size="sm" class="text-neutral-400" />
|
|
91
|
+
</template>
|
|
92
|
+
</FListItem>
|
|
93
|
+
`
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const WithBadge = () => ({
|
|
97
|
+
components: { FListItem, FIcon, FBadge },
|
|
98
|
+
template: `
|
|
99
|
+
<FListItem title="Messages" subtitle="Consultez vos messages" clickable>
|
|
100
|
+
<template #left>
|
|
101
|
+
<FIcon name="mail" size="md" class="text-neutral-500" />
|
|
102
|
+
</template>
|
|
103
|
+
<template #right>
|
|
104
|
+
<FBadge content="3" variant="error" shape="circle" />
|
|
105
|
+
</template>
|
|
106
|
+
</FListItem>
|
|
107
|
+
`
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export const List = () => ({
|
|
111
|
+
components: { FListItem, FAvatar },
|
|
112
|
+
data() {
|
|
113
|
+
return {
|
|
114
|
+
selectedId: 2,
|
|
115
|
+
users: [
|
|
116
|
+
{ id: 1, name: 'Alice Martin', role: 'Designer' },
|
|
117
|
+
{ id: 2, name: 'Bob Dupont', role: 'Développeur' },
|
|
118
|
+
{ id: 3, name: 'Claire Durand', role: 'Product Manager' },
|
|
119
|
+
{ id: 4, name: 'David Petit', role: 'DevOps' }
|
|
120
|
+
]
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
methods: {
|
|
124
|
+
selectUser(id) {
|
|
125
|
+
this.selectedId = id;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
template: `
|
|
129
|
+
<div class="divide-y divide-neutral-100">
|
|
130
|
+
<FListItem
|
|
131
|
+
v-for="user in users"
|
|
132
|
+
:key="user.id"
|
|
133
|
+
:title="user.name"
|
|
134
|
+
:subtitle="user.role"
|
|
135
|
+
:selected="selectedId === user.id"
|
|
136
|
+
clickable
|
|
137
|
+
@click="selectUser(user.id)"
|
|
138
|
+
>
|
|
139
|
+
<template #left>
|
|
140
|
+
<FAvatar :name="user.name" size="sm" />
|
|
141
|
+
</template>
|
|
142
|
+
</FListItem>
|
|
143
|
+
</div>
|
|
144
|
+
`
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export const LongContent = () => ({
|
|
148
|
+
components: { FListItem },
|
|
149
|
+
template: `
|
|
150
|
+
<div class="w-64">
|
|
151
|
+
<FListItem
|
|
152
|
+
title="Un titre très long qui devrait être tronqué"
|
|
153
|
+
subtitle="Une description également très longue qui sera tronquée"
|
|
154
|
+
truncate
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
`
|
|
158
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import FListItem from './FListItem.vue';
|
|
4
|
+
|
|
5
|
+
describe('FListItem', () => {
|
|
6
|
+
it('renders correctly with default props', () => {
|
|
7
|
+
const wrapper = mount(FListItem);
|
|
8
|
+
expect(wrapper.exists()).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('displays title when provided', () => {
|
|
12
|
+
const wrapper = mount(FListItem, {
|
|
13
|
+
propsData: { title: 'Item Title' }
|
|
14
|
+
});
|
|
15
|
+
expect(wrapper.text()).toContain('Item Title');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('displays subtitle when provided', () => {
|
|
19
|
+
const wrapper = mount(FListItem, {
|
|
20
|
+
propsData: { title: 'Title', subtitle: 'Subtitle text' }
|
|
21
|
+
});
|
|
22
|
+
expect(wrapper.text()).toContain('Subtitle text');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('is not clickable by default', () => {
|
|
26
|
+
const wrapper = mount(FListItem);
|
|
27
|
+
expect(wrapper.attributes('role')).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('is clickable when clickable prop is true', () => {
|
|
31
|
+
const wrapper = mount(FListItem, {
|
|
32
|
+
propsData: { clickable: true }
|
|
33
|
+
});
|
|
34
|
+
expect(wrapper.attributes('role')).toBe('button');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('emits click event when clickable and clicked', async () => {
|
|
38
|
+
const wrapper = mount(FListItem, {
|
|
39
|
+
propsData: { clickable: true }
|
|
40
|
+
});
|
|
41
|
+
await wrapper.trigger('click');
|
|
42
|
+
expect(wrapper.emitted('click')).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not emit click when not clickable', async () => {
|
|
46
|
+
const wrapper = mount(FListItem);
|
|
47
|
+
await wrapper.trigger('click');
|
|
48
|
+
expect(wrapper.emitted('click')).toBeFalsy();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('does not emit click when disabled', async () => {
|
|
52
|
+
const wrapper = mount(FListItem, {
|
|
53
|
+
propsData: { clickable: true, disabled: true }
|
|
54
|
+
});
|
|
55
|
+
await wrapper.trigger('click');
|
|
56
|
+
expect(wrapper.emitted('click')).toBeFalsy();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('applies selected styles when selected', () => {
|
|
60
|
+
const wrapper = mount(FListItem, {
|
|
61
|
+
propsData: { selected: true }
|
|
62
|
+
});
|
|
63
|
+
expect(wrapper.classes().join(' ')).toContain('bg-primary-50');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders left slot', () => {
|
|
67
|
+
const wrapper = mount(FListItem, {
|
|
68
|
+
slots: { left: '<span>Left content</span>' }
|
|
69
|
+
});
|
|
70
|
+
expect(wrapper.html()).toContain('Left content');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('renders right slot', () => {
|
|
74
|
+
const wrapper = mount(FListItem, {
|
|
75
|
+
slots: { right: '<span>Right content</span>' }
|
|
76
|
+
});
|
|
77
|
+
expect(wrapper.html()).toContain('Right content');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('renders content slot', () => {
|
|
81
|
+
const wrapper = mount(FListItem, {
|
|
82
|
+
slots: { content: '<div>Custom content</div>' }
|
|
83
|
+
});
|
|
84
|
+
expect(wrapper.html()).toContain('Custom content');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('applies truncate to text when truncate is true', () => {
|
|
88
|
+
const wrapper = mount(FListItem, {
|
|
89
|
+
propsData: { title: 'Long title', truncate: true }
|
|
90
|
+
});
|
|
91
|
+
expect(wrapper.exists()).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="listItemClasses"
|
|
4
|
+
:tabindex="clickable ? 0 : undefined"
|
|
5
|
+
:role="clickable ? 'button' : undefined"
|
|
6
|
+
@click="handleClick"
|
|
7
|
+
@keydown.enter="handleClick"
|
|
8
|
+
@keydown.space.prevent="handleClick"
|
|
9
|
+
>
|
|
10
|
+
<div v-if="$slots.left" class="flex-shrink-0">
|
|
11
|
+
<slot name="left" />
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="flex-1 min-w-0">
|
|
15
|
+
<f-typography
|
|
16
|
+
v-if="title"
|
|
17
|
+
variant="body"
|
|
18
|
+
:truncate="truncate"
|
|
19
|
+
:class="titleClasses"
|
|
20
|
+
>
|
|
21
|
+
{{ title }}
|
|
22
|
+
</f-typography>
|
|
23
|
+
<f-typography
|
|
24
|
+
v-if="subtitle"
|
|
25
|
+
variant="caption"
|
|
26
|
+
:truncate="truncate"
|
|
27
|
+
:class="subtitleClasses"
|
|
28
|
+
>
|
|
29
|
+
{{ subtitle }}
|
|
30
|
+
</f-typography>
|
|
31
|
+
<slot name="content" />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div v-if="$slots.right" class="flex-shrink-0">
|
|
35
|
+
<slot name="right" />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script>
|
|
41
|
+
import FTypography from '../../atoms/FTypography/FTypography.vue';
|
|
42
|
+
|
|
43
|
+
export default {
|
|
44
|
+
name: 'FListItem',
|
|
45
|
+
components: {
|
|
46
|
+
FTypography
|
|
47
|
+
},
|
|
48
|
+
props: {
|
|
49
|
+
title: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: ''
|
|
52
|
+
},
|
|
53
|
+
subtitle: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: ''
|
|
56
|
+
},
|
|
57
|
+
clickable: {
|
|
58
|
+
type: Boolean,
|
|
59
|
+
default: false
|
|
60
|
+
},
|
|
61
|
+
selected: {
|
|
62
|
+
type: Boolean,
|
|
63
|
+
default: false
|
|
64
|
+
},
|
|
65
|
+
disabled: {
|
|
66
|
+
type: Boolean,
|
|
67
|
+
default: false
|
|
68
|
+
},
|
|
69
|
+
truncate: {
|
|
70
|
+
type: Boolean,
|
|
71
|
+
default: true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
computed: {
|
|
75
|
+
listItemClasses() {
|
|
76
|
+
const baseClasses = 'flex items-center gap-3 px-4 py-3';
|
|
77
|
+
const transitionClasses =
|
|
78
|
+
'transition-all duration-[var(--transition-duration-base)] ease-[var(--transition-easing-standard)]';
|
|
79
|
+
const clickableClasses =
|
|
80
|
+
this.clickable && !this.disabled
|
|
81
|
+
? 'cursor-pointer hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-primary-500/20'
|
|
82
|
+
: '';
|
|
83
|
+
const selectedClasses = this.selected ? 'bg-primary-50' : '';
|
|
84
|
+
const disabledClasses = this.disabled
|
|
85
|
+
? 'opacity-50 cursor-not-allowed'
|
|
86
|
+
: '';
|
|
87
|
+
|
|
88
|
+
return [
|
|
89
|
+
baseClasses,
|
|
90
|
+
transitionClasses,
|
|
91
|
+
clickableClasses,
|
|
92
|
+
selectedClasses,
|
|
93
|
+
disabledClasses
|
|
94
|
+
]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(' ');
|
|
97
|
+
},
|
|
98
|
+
titleClasses() {
|
|
99
|
+
return this.disabled ? 'text-neutral-400' : '';
|
|
100
|
+
},
|
|
101
|
+
subtitleClasses() {
|
|
102
|
+
return this.disabled ? 'text-neutral-300' : '';
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
methods: {
|
|
106
|
+
handleClick(event) {
|
|
107
|
+
if (!this.disabled && this.clickable) {
|
|
108
|
+
this.$emit('click', event);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
</script>
|