@cnamts/synapse 0.0.6-alpha → 0.0.8-alpha
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/dist/design-system-v3.d.ts +331 -372
- package/dist/design-system-v3.js +2794 -2637
- package/dist/design-system-v3.umd.cjs +1 -10
- package/dist/style.css +1 -1
- package/package.json +10 -2
- package/src/assets/settings.scss +2 -2
- package/src/assets/tokens.scss +107 -112
- package/src/components/BackBtn/BackBtn.stories.ts +4 -1
- package/src/components/BackBtn/BackBtn.vue +4 -4
- package/src/components/BackToTopBtn/BackToTopBtn.stories.ts +3 -3
- package/src/components/BackToTopBtn/BackToTopBtn.vue +1 -0
- package/src/components/CollapsibleList/CollapsibleList.mdx +1 -1
- package/src/components/CollapsibleList/CollapsibleList.vue +43 -44
- package/src/components/ContextualMenu/ContextualMenu.mdx +118 -0
- package/src/components/ContextualMenu/ContextualMenu.stories.ts +430 -0
- package/src/components/ContextualMenu/ContextualMenu.vue +101 -0
- package/src/components/ContextualMenu/tests/ContextualMenu.spec.ts +115 -0
- package/src/components/ContextualMenu/tests/__snapshots__/ContextualMenu.spec.ts.snap +10 -0
- package/src/components/ContextualMenu/types.ts +5 -0
- package/src/components/CookieBanner/CookieBanner.stories.ts +3 -2
- package/src/components/CookieBanner/CookieBanner.vue +13 -10
- package/src/components/CookieBanner/tests/__snapshots__/CookieBanner.spec.ts.snap +17 -15
- package/src/components/CookiesSelection/CookiesInformation/CookiesInformation.vue +6 -1
- package/src/components/CookiesSelection/CookiesInformation/locales.ts +1 -0
- package/src/components/CookiesSelection/CookiesTable/CookiesTable.vue +1 -0
- package/src/components/CookiesSelection/tests/__snapshots__/CookiesSelection.spec.ts.snap +17 -15
- package/src/components/CopyBtn/CopyBtn.stories.ts +5 -2
- package/src/components/CopyBtn/CopyBtn.vue +7 -7
- package/src/components/Customs/SyBtnSelect/SyBtnSelect.stories.ts +11 -51
- package/src/components/Customs/SyBtnSelect/SyBtnSelect.vue +26 -26
- package/src/components/Customs/SyInputSelect/SyInputSelect.mdx +4 -1
- package/src/components/Customs/SyInputSelect/SyInputSelect.stories.ts +19 -7
- package/src/components/Customs/SyInputSelect/SyInputSelect.vue +24 -24
- package/src/components/Customs/SySelect/SySelect.mdx +1 -2
- package/src/components/Customs/SySelect/SySelect.stories.ts +0 -2
- package/src/components/Customs/SySelect/SySelect.vue +27 -26
- package/src/components/DataList/DataList.stories.ts +3 -2
- package/src/components/DataList/DataList.vue +1 -1
- package/src/components/DataListGroup/DataListGroup.stories.ts +3 -2
- package/src/components/DataListItem/DataListItem.vue +12 -12
- package/src/components/DialogBox/DialogBox.mdx +28 -2
- package/src/components/DialogBox/DialogBox.stories.ts +1 -1
- package/src/components/DialogBox/DialogBox.vue +3 -2
- package/src/components/DownloadBtn/DownloadBtn.mdx +3 -4
- package/src/components/DownloadBtn/DownloadBtn.stories.ts +20 -21
- package/src/components/DownloadBtn/DownloadBtn.vue +2 -1
- package/src/components/ErrorPage/ErrorPage.vue +1 -1
- package/src/components/ExternalLinks/ExternalLinks.mdx +86 -0
- package/src/components/ExternalLinks/ExternalLinks.stories.ts +553 -0
- package/src/components/ExternalLinks/ExternalLinks.vue +200 -0
- package/src/components/ExternalLinks/config.ts +34 -0
- package/src/components/ExternalLinks/locales.ts +4 -0
- package/src/components/ExternalLinks/tests/ExternalLinks.spec.ts +154 -0
- package/src/components/ExternalLinks/tests/__snapshots__/ExternalLinks.spec.ts.snap +159 -0
- package/src/components/FooterBar/FooterBar.vue +111 -82
- package/src/components/FranceConnectBtn/FranceConnectBtn.stories.ts +2 -1
- package/src/components/FranceConnectBtn/FranceConnectBtn.vue +14 -13
- package/src/components/HeaderBar/HeaderBar.stories.ts +19 -12
- package/src/components/HeaderBar/HeaderBar.vue +8 -3
- package/src/components/HeaderBar/HeaderBurgerMenu/HeaderBurgerMenu.vue +12 -7
- package/src/components/HeaderBar/HeaderBurgerMenu/HeaderMenuItem/HeaderMenuItem.vue +5 -5
- package/src/components/HeaderBar/HeaderBurgerMenu/HeaderMenuSection/HeaderMenuSection.vue +2 -2
- package/src/components/HeaderBar/HeaderBurgerMenu/HeaderSubMenu/HeaderSubMenu.vue +10 -8
- package/src/components/HeaderBar/HeaderLogo/HeaderLogo.vue +2 -2
- package/src/components/HeaderBar/HeaderLogo/logos/Logo-mobile.vue +2 -1
- package/src/components/HeaderBar/HeaderLogo/logos/Logo.vue +2 -1
- package/src/components/HeaderBar/HeaderMenuBtn/HeaderMenuBtn.vue +10 -10
- package/src/components/HeaderBar/consts.scss +1 -1
- package/src/components/HeaderLoading/HeaderLoading.vue +12 -11
- package/src/components/HeaderNavigationBar/HeaderNavigationBar.stories.ts +104 -32
- package/src/components/HeaderNavigationBar/HeaderNavigationBar.vue +2 -1
- package/src/components/HeaderNavigationBar/HorizontalNavbar/HorizontalNavbar.vue +9 -9
- package/src/components/HeaderToolbar/HeaderToolbar.vue +215 -180
- package/src/components/LangBtn/LangBtn.vue +8 -6
- package/src/components/LogoBrandSection/LogoBrandSection.stories.ts +2 -2
- package/src/components/NirField/NirField.mdx +1 -4
- package/src/components/NirField/NirField.stories.ts +71 -18
- package/src/components/NirField/NirField.vue +49 -49
- package/src/components/NirField/tests/NirField.spec.ts +1 -0
- package/src/components/NotFoundPage/NotFoundPage.stories.ts +33 -2
- package/src/components/NotFoundPage/NotFoundPage.vue +17 -0
- package/src/components/NotificationBar/NotificationBar.mdx +5 -5
- package/src/components/NotificationBar/NotificationBar.stories.ts +410 -314
- package/src/components/NotificationBar/NotificationBar.vue +43 -41
- package/src/components/PageContainer/PageContainer.stories.ts +5 -5
- package/src/components/PageContainer/PageContainer.vue +13 -8
- package/src/components/PageContainer/tests/PageContainer.spec.ts +1 -1
- package/src/components/PasswordField/PasswordField.mdx +70 -0
- package/src/components/PasswordField/PasswordField.stories.ts +213 -0
- package/src/components/PasswordField/PasswordField.vue +189 -0
- package/src/components/PasswordField/config.ts +11 -0
- package/src/components/PasswordField/locales.ts +4 -0
- package/src/components/PasswordField/tests/PasswordField.spec.ts +96 -0
- package/src/components/PhoneField/PhoneField.mdx +0 -2
- package/src/components/PhoneField/PhoneField.stories.ts +10 -50
- package/src/components/PhoneField/PhoneField.vue +34 -34
- package/src/components/SkipLink/SkipLink.vue +10 -10
- package/src/components/SocialMediaLinks/SocialMediaLinks.vue +29 -21
- package/src/components/SocialMediaLinks/tests/__snapshots__/SocialMediaLinks.spec.ts.snap +2 -2
- package/src/components/SubHeader/SubHeader.stories.ts +6 -3
- package/src/components/SubHeader/SubHeader.vue +32 -31
- package/src/components/SyAlert/SyAlert.vue +15 -8
- package/src/components/UserMenuBtn/UserMenuBtn.vue +1 -1
- package/src/components/UserMenuBtn/config.ts +1 -1
- package/src/components/index.ts +10 -6
- package/src/designTokens/index.ts +6 -4
- package/src/designTokens/{bootstrapColors.md → paColors.md} +1 -1
- package/src/designTokens/tokens/cnam/cnamLightTheme.ts +2 -0
- package/src/designTokens/tokens/pa/paColors.ts +171 -0
- package/src/designTokens/tokens/pa/paContextual.ts +58 -0
- package/src/designTokens/tokens/pa/paDarkTheme.ts +5 -0
- package/src/designTokens/tokens/pa/paLightTheme.ts +123 -0
- package/src/designTokens/tokens/pa/paSemantic.ts +87 -0
- package/src/stories/GuideDuDev/CreerUneIssue.mdx +64 -0
- package/src/stories/GuideDuDev/{CommentUtiliserLesRules.mdx → UtiliserLesRules.mdx} +2 -2
- package/src/stories/GuideDuDev/components.stories.ts +9 -7
- package/src/stories/Guidelines/Vuetify/Vuetify.stories.ts +163 -88
- package/src/stories/Guidelines/Vuetify/VuetifyItems.ts +250 -23
- package/src/temp/TestDTComponent.vue +5 -6
- package/src/designTokens/tokens/bootstrap/bootstrapColors.ts +0 -158
- package/src/designTokens/tokens/bootstrap/bootstrapLightTheme.ts +0 -22
- package/src/stories/GuideDuDev/CommentContribuer.mdx +0 -22
|
@@ -203,8 +203,8 @@
|
|
|
203
203
|
<template #actions>
|
|
204
204
|
<div
|
|
205
205
|
class="d-flex ga-2"
|
|
206
|
-
style="width:100
|
|
207
|
-
:class="hasLongContent ? 'action-section-
|
|
206
|
+
style="width: 100%;"
|
|
207
|
+
:class="hasLongContent ? 'action-section-long-text' : 'action-section-short-text'"
|
|
208
208
|
>
|
|
209
209
|
<slot name="action" />
|
|
210
210
|
<VBtn
|
|
@@ -228,67 +228,69 @@
|
|
|
228
228
|
</template>
|
|
229
229
|
|
|
230
230
|
<style lang="scss" scoped>
|
|
231
|
-
@use '@/assets/tokens
|
|
231
|
+
@use '@/assets/tokens';
|
|
232
232
|
|
|
233
233
|
.vd-notification-content {
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
display: flex;
|
|
235
|
+
align-items: center;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
.vd-notification-bar :deep(.v-snack__wrapper) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
padding: 16px;
|
|
240
|
+
min-width: 0;
|
|
241
|
+
max-width: none;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
:deep(.v-snackbar__content) {
|
|
245
|
-
|
|
245
|
+
padding: tokens.$padding-4 !important;
|
|
246
246
|
}
|
|
247
|
+
|
|
247
248
|
:deep(.v-snackbar__actions) {
|
|
248
|
-
|
|
249
|
+
margin-inline-end: 10px;
|
|
249
250
|
}
|
|
250
251
|
|
|
251
252
|
.vd-notification-bar.v-snackbar--vertical :deep() {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
253
|
+
.v-snackbar--vertical .v-snackbar__wrapper .v-snackbar__actions {
|
|
254
|
+
width: 100% !important;
|
|
255
|
+
align-self: auto;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.v-snack__wrapper {
|
|
259
|
+
align-items: stretch;
|
|
260
|
+
flex-direction: row;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.v-snack__action {
|
|
264
|
+
align-self: stretch;
|
|
265
|
+
align-items: stretch;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.v-snackbar__content {
|
|
270
|
+
margin: 0;
|
|
271
|
+
width: 100%;
|
|
272
|
+
display: flex;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.vd-notification-content {
|
|
276
|
+
flex-direction: column;
|
|
277
|
+
display: flex;
|
|
278
|
+
}
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
.long-text :deep(.v-snackbar__actions) {
|
|
280
|
-
|
|
282
|
+
width: 98% !important;
|
|
281
283
|
}
|
|
282
284
|
|
|
283
285
|
.short-text :deep(.v-snackbar__actions) {
|
|
284
|
-
|
|
286
|
+
width: 48% !important;
|
|
285
287
|
}
|
|
286
288
|
|
|
287
|
-
.action-section-
|
|
288
|
-
|
|
289
|
+
.action-section-long-text {
|
|
290
|
+
justify-content: space-around;
|
|
289
291
|
}
|
|
290
292
|
|
|
291
|
-
.action-section-
|
|
292
|
-
|
|
293
|
+
.action-section-short-text {
|
|
294
|
+
justify-content: end !important;
|
|
293
295
|
}
|
|
294
296
|
</style>
|
|
@@ -11,12 +11,12 @@ const meta = {
|
|
|
11
11
|
},
|
|
12
12
|
argTypes: {
|
|
13
13
|
size: {
|
|
14
|
-
options: ['xl', '
|
|
14
|
+
options: ['xl', 'lg', 'md', 'sm', 'xs'],
|
|
15
15
|
control: { type: 'select' },
|
|
16
|
-
default:
|
|
16
|
+
default: undefined,
|
|
17
17
|
},
|
|
18
18
|
spacing: {
|
|
19
|
-
options: ['
|
|
19
|
+
options: ['xl', 'lg', 'md', 'sm', 'xs'],
|
|
20
20
|
control: { type: 'select' },
|
|
21
21
|
default: undefined,
|
|
22
22
|
},
|
|
@@ -77,7 +77,7 @@ export const Size: Story = {
|
|
|
77
77
|
{
|
|
78
78
|
name: 'Template',
|
|
79
79
|
code: `<template>
|
|
80
|
-
<PageContainer size="
|
|
80
|
+
<PageContainer size="sm">
|
|
81
81
|
Contenu de la page
|
|
82
82
|
</PageContainer>
|
|
83
83
|
</template>
|
|
@@ -94,7 +94,7 @@ export const Size: Story = {
|
|
|
94
94
|
},
|
|
95
95
|
args: {
|
|
96
96
|
default: 'Contenu de la page',
|
|
97
|
-
size: '
|
|
97
|
+
size: 'sm',
|
|
98
98
|
},
|
|
99
99
|
render: (args) => {
|
|
100
100
|
return {
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
import { useDisplay } from 'vuetify'
|
|
4
4
|
|
|
5
5
|
const props = withDefaults(defineProps<{
|
|
6
|
-
size?: 'xl' | '
|
|
7
|
-
spacing?: '
|
|
6
|
+
size?: 'xl' | 'lg' | 'md' | 'sm' | 'xs'
|
|
7
|
+
spacing?: 'xl' | 'lg' | 'md' | 'sm' | 'xs'
|
|
8
8
|
color?: string
|
|
9
9
|
}>(), {
|
|
10
|
-
size:
|
|
10
|
+
size: undefined,
|
|
11
11
|
spacing: undefined,
|
|
12
12
|
color: 'transparent',
|
|
13
13
|
})
|
|
@@ -38,7 +38,12 @@
|
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
const containerSize = computed(() => {
|
|
41
|
-
|
|
41
|
+
if (props.size) {
|
|
42
|
+
return sizeMapping[props.size]
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
return sizeMapping[display.name.value] ?? sizeMapping['xl']
|
|
46
|
+
}
|
|
42
47
|
})
|
|
43
48
|
|
|
44
49
|
defineExpose({
|
|
@@ -60,9 +65,9 @@
|
|
|
60
65
|
|
|
61
66
|
<style lang="scss" scoped>
|
|
62
67
|
.vd-page-container {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
flex: 1;
|
|
69
|
+
width: 100%;
|
|
70
|
+
max-width: 1712px;
|
|
71
|
+
margin: 0 auto;
|
|
67
72
|
}
|
|
68
73
|
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {Canvas, Meta, Controls, Source} from '@storybook/blocks';
|
|
2
|
+
import * as PasswordFieldStories from './PasswordField.stories';
|
|
3
|
+
import PasswordField from './PasswordField.vue';
|
|
4
|
+
|
|
5
|
+
<Meta title="Composants/Formulaires/PasswordField" component={PasswordField}/>
|
|
6
|
+
|
|
7
|
+
# PasswordField
|
|
8
|
+
|
|
9
|
+
Le composant `PasswordField` est utilisé pour afficher un champ de saisie de mot de passe et gérer sa validation.
|
|
10
|
+
Il permet également d’afficher ou de masquer le contenu du champ à l’aide d’une icône.
|
|
11
|
+
|
|
12
|
+
<Canvas story={{height: '150px'}} of={PasswordFieldStories.Default}/>
|
|
13
|
+
|
|
14
|
+
# API
|
|
15
|
+
|
|
16
|
+
<Controls of={PasswordFieldStories.Default}/>
|
|
17
|
+
|
|
18
|
+
## Utilisation de base
|
|
19
|
+
|
|
20
|
+
<Source
|
|
21
|
+
dark
|
|
22
|
+
code={`
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { ref } from 'vue'
|
|
25
|
+
import { PasswordField } from '@cnamts/synapse'
|
|
26
|
+
|
|
27
|
+
const password = ref('')
|
|
28
|
+
const passwordFieldRef = ref() // Référence Vue pour accéder au composant enfant
|
|
29
|
+
|
|
30
|
+
function handleSubmit() {
|
|
31
|
+
// Appeler la méthode exposée validateOnSubmit via la référence
|
|
32
|
+
const isValid = passwordFieldRef.value?.validateOnSubmit()
|
|
33
|
+
if (!isValid) {
|
|
34
|
+
alert('Veuillez corriger les erreurs avant de soumettre.')
|
|
35
|
+
} else {
|
|
36
|
+
alert('Formulaire soumis avec succès !')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<form @submit.prevent="handleSubmit">
|
|
43
|
+
<PasswordField
|
|
44
|
+
ref="passwordFieldRef"
|
|
45
|
+
v-model="password"
|
|
46
|
+
outlined
|
|
47
|
+
:is-validate-on-blur="true"
|
|
48
|
+
/>
|
|
49
|
+
<button type="submit">Soumettre</button>
|
|
50
|
+
</form>
|
|
51
|
+
</template>
|
|
52
|
+
`}
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
## Gestion de la validation
|
|
56
|
+
|
|
57
|
+
### Validation par défaut
|
|
58
|
+
|
|
59
|
+
- **required** : Si `true`, le champ est obligatoire et affiche une erreur si le champ est vide.
|
|
60
|
+
- **isValidateOnBlur** : Si `true`, la validation se déclenche automatiquement lors du blur (perte de focus).
|
|
61
|
+
|
|
62
|
+
### Règles de validation personnalisées (props `customRules`)
|
|
63
|
+
|
|
64
|
+
Vous pouvez définir des règles de validation personnalisées sous forme de tableau d’objets.
|
|
65
|
+
|
|
66
|
+
Pour savoir comment utiliser les règles personnalisées, veuillez consulter la section [Comment utiliser les rules](/docs/guide-du-dev-comment-utiliser-les-rules--docs).
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { StoryObj, Meta } from '@storybook/vue3'
|
|
2
|
+
import PasswordField from './PasswordField.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Composants/Formulaires/PasswordField',
|
|
6
|
+
component: PasswordField,
|
|
7
|
+
decorators: [
|
|
8
|
+
() => ({
|
|
9
|
+
template: '<div style="padding: 20px;"><story/></div>',
|
|
10
|
+
}),
|
|
11
|
+
],
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'fullscreen',
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
modelValue: {
|
|
17
|
+
description: 'La valeur du modèle pour le champ.',
|
|
18
|
+
control: 'text',
|
|
19
|
+
default: null,
|
|
20
|
+
table: {
|
|
21
|
+
type: {
|
|
22
|
+
summary: 'string | null',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
outlined: {
|
|
27
|
+
description: 'Définit la variante du champ (outlined ou underlined).',
|
|
28
|
+
control: 'boolean',
|
|
29
|
+
default: true,
|
|
30
|
+
table: {
|
|
31
|
+
type: {
|
|
32
|
+
summary: 'boolean',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: {
|
|
37
|
+
description: 'Indique si le champ est requis.',
|
|
38
|
+
control: 'boolean',
|
|
39
|
+
default: false,
|
|
40
|
+
table: {
|
|
41
|
+
type: {
|
|
42
|
+
summary: 'boolean',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
isValidateOnBlur: {
|
|
47
|
+
description: 'Active ou non la validation lors du blur.',
|
|
48
|
+
control: 'boolean',
|
|
49
|
+
default: true,
|
|
50
|
+
table: {
|
|
51
|
+
type: {
|
|
52
|
+
summary: 'boolean',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
customRules: {
|
|
57
|
+
description: 'Règles de validation personnalisées.',
|
|
58
|
+
control: 'object',
|
|
59
|
+
table: {
|
|
60
|
+
type: {
|
|
61
|
+
summary: 'array',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
} satisfies Meta<typeof PasswordField>
|
|
67
|
+
|
|
68
|
+
export default meta
|
|
69
|
+
|
|
70
|
+
type Story = StoryObj<typeof meta>
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Story par défaut
|
|
74
|
+
*/
|
|
75
|
+
export const Default: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
modelValue: '',
|
|
78
|
+
outlined: true,
|
|
79
|
+
required: false,
|
|
80
|
+
isValidateOnBlur: true,
|
|
81
|
+
customRules: [],
|
|
82
|
+
},
|
|
83
|
+
render: (args) => {
|
|
84
|
+
return {
|
|
85
|
+
components: { PasswordField },
|
|
86
|
+
setup() {
|
|
87
|
+
return { args }
|
|
88
|
+
},
|
|
89
|
+
template: `
|
|
90
|
+
<PasswordField v-bind="args" v-model="args.modelValue"/>
|
|
91
|
+
`,
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
parameters: {
|
|
95
|
+
sourceCode: [
|
|
96
|
+
{
|
|
97
|
+
name: 'Template',
|
|
98
|
+
code: `
|
|
99
|
+
<template>
|
|
100
|
+
<PasswordField
|
|
101
|
+
v-model="password"
|
|
102
|
+
:required="false"
|
|
103
|
+
:isValidateOnBlur="true"
|
|
104
|
+
/>
|
|
105
|
+
</template>
|
|
106
|
+
`,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'Script',
|
|
110
|
+
code: `
|
|
111
|
+
<script setup lang="ts">
|
|
112
|
+
import { ref } from 'vue'
|
|
113
|
+
import PasswordField from '@cnamts/synapse'
|
|
114
|
+
|
|
115
|
+
const password = ref('')
|
|
116
|
+
</script>
|
|
117
|
+
`,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Story avec champ requis
|
|
125
|
+
*/
|
|
126
|
+
export const Required: Story = {
|
|
127
|
+
args: {
|
|
128
|
+
...Default.args,
|
|
129
|
+
required: true,
|
|
130
|
+
},
|
|
131
|
+
parameters: {
|
|
132
|
+
...Default.parameters,
|
|
133
|
+
sourceCode: [
|
|
134
|
+
{
|
|
135
|
+
name: 'Template',
|
|
136
|
+
code: `
|
|
137
|
+
<template>
|
|
138
|
+
<PasswordField
|
|
139
|
+
v-model="password"
|
|
140
|
+
:required="true"
|
|
141
|
+
:isValidateOnBlur="true"
|
|
142
|
+
/>
|
|
143
|
+
</template>
|
|
144
|
+
`,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'Script',
|
|
148
|
+
code: `
|
|
149
|
+
<script setup lang="ts">
|
|
150
|
+
import { ref } from 'vue'
|
|
151
|
+
import PasswordField from '@cnamts/synapse'
|
|
152
|
+
|
|
153
|
+
const password = ref('')
|
|
154
|
+
</script>
|
|
155
|
+
`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const WithCustomRules: Story = {
|
|
162
|
+
args: {
|
|
163
|
+
...Default.args,
|
|
164
|
+
customRules: [
|
|
165
|
+
{
|
|
166
|
+
type: 'minLength',
|
|
167
|
+
options: {
|
|
168
|
+
length: 8,
|
|
169
|
+
message: 'Le mot de passe doit comporter au moins 8 caractères.',
|
|
170
|
+
successMessage: 'Le mot de passe est suffisamment long.',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
parameters: {
|
|
176
|
+
...Default.parameters,
|
|
177
|
+
sourceCode: [
|
|
178
|
+
{
|
|
179
|
+
name: 'Template',
|
|
180
|
+
code: `
|
|
181
|
+
<template>
|
|
182
|
+
<PasswordField
|
|
183
|
+
v-model="password"
|
|
184
|
+
:required="false"
|
|
185
|
+
:isValidateOnBlur="true"
|
|
186
|
+
:customRules="[
|
|
187
|
+
{
|
|
188
|
+
type: 'minLength',
|
|
189
|
+
options: {
|
|
190
|
+
length: 8,
|
|
191
|
+
message: 'Le mot de passe doit comporter au moins 8 caractères.',
|
|
192
|
+
successMessage: 'Le mot de passe est suffisamment long.'
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
]"
|
|
196
|
+
/>
|
|
197
|
+
</template>
|
|
198
|
+
`,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'Script',
|
|
202
|
+
code: `
|
|
203
|
+
<script setup lang="ts">
|
|
204
|
+
import { ref } from 'vue'
|
|
205
|
+
import PasswordField from '@cnamts/synapse'
|
|
206
|
+
|
|
207
|
+
const password = ref('')
|
|
208
|
+
</script>
|
|
209
|
+
`,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, computed, watch } from 'vue'
|
|
3
|
+
import { config } from './config'
|
|
4
|
+
import { locales } from './locales'
|
|
5
|
+
import { useFieldValidation } from '@/composables/rules/useFieldValidation'
|
|
6
|
+
import { mdiEye, mdiEyeOff } from '@mdi/js'
|
|
7
|
+
// import deepMerge from 'deepmerge'
|
|
8
|
+
import useCustomizableOptions, { type CustomizableOptions } from '@/composables/useCustomizableOptions'
|
|
9
|
+
|
|
10
|
+
type Rule = (value: string | null) => { error?: string, success?: string }
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<{
|
|
13
|
+
modelValue?: string | null
|
|
14
|
+
outlined?: boolean
|
|
15
|
+
required?: boolean
|
|
16
|
+
isValidateOnBlur?: boolean
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
|
|
18
|
+
customRules?: any
|
|
19
|
+
} & CustomizableOptions>(), {
|
|
20
|
+
modelValue: null,
|
|
21
|
+
outlined: true,
|
|
22
|
+
required: false,
|
|
23
|
+
isValidateOnBlur: true,
|
|
24
|
+
customRules: [],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const options = useCustomizableOptions(config, props)
|
|
28
|
+
const emit = defineEmits(['update:modelValue', 'submit'])
|
|
29
|
+
|
|
30
|
+
const eyeIcon = mdiEye
|
|
31
|
+
const eyeOffIcon = mdiEyeOff
|
|
32
|
+
const showEyeIcon = ref(false)
|
|
33
|
+
|
|
34
|
+
const btnLabel = computed(() => {
|
|
35
|
+
return showEyeIcon.value ? locales.hidePassword : locales.showPassword
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const password = ref<string | null>(props.modelValue)
|
|
39
|
+
watch(
|
|
40
|
+
() => props.modelValue,
|
|
41
|
+
(newVal) => {
|
|
42
|
+
password.value = newVal
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const { generateRules } = useFieldValidation()
|
|
47
|
+
|
|
48
|
+
const defaultRules = [
|
|
49
|
+
...(props.required
|
|
50
|
+
? [{
|
|
51
|
+
type: 'required',
|
|
52
|
+
options: { message: 'Le mot de passe est requis.', fieldIdentifier: 'password' },
|
|
53
|
+
}]
|
|
54
|
+
: []),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const rules = computed(() => {
|
|
58
|
+
const baseRules = (props.required ? defaultRules : [])
|
|
59
|
+
return props.customRules ? generateRules([...baseRules, ...props.customRules]) : generateRules(baseRules)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const errors = ref<string[]>([])
|
|
63
|
+
const successes = ref<string[]>([])
|
|
64
|
+
|
|
65
|
+
const isValidating = ref(false)
|
|
66
|
+
|
|
67
|
+
watch(() => password.value, () => {
|
|
68
|
+
validateFields()
|
|
69
|
+
}, { immediate: true })
|
|
70
|
+
|
|
71
|
+
watch(
|
|
72
|
+
() => props.isValidateOnBlur,
|
|
73
|
+
() => {
|
|
74
|
+
validateFields()
|
|
75
|
+
},
|
|
76
|
+
{ immediate: true },
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
watch(
|
|
80
|
+
() => props.required,
|
|
81
|
+
() => {
|
|
82
|
+
validateFields()
|
|
83
|
+
},
|
|
84
|
+
{ immediate: true },
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
function validateFieldSet(value: string | null, rules: Rule[]) {
|
|
88
|
+
rules.forEach((rule) => {
|
|
89
|
+
const { error, success } = rule(value)
|
|
90
|
+
if (error) errors.value.push(error)
|
|
91
|
+
if (success && success !== 'Le champ est valide.') successes.value.push(success)
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateFields(onBlur = false): void {
|
|
96
|
+
errors.value = []
|
|
97
|
+
successes.value = []
|
|
98
|
+
|
|
99
|
+
const shouldValidate = onBlur || !props.isValidateOnBlur
|
|
100
|
+
|
|
101
|
+
if (!shouldValidate) return
|
|
102
|
+
|
|
103
|
+
validateFieldSet(password.value, rules.value)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function emitChangeEvent(value: string): void {
|
|
107
|
+
emit('update:modelValue', value)
|
|
108
|
+
validateFields()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
112
|
+
if (event.key === 'Enter') {
|
|
113
|
+
emit('submit')
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateOnSubmit() {
|
|
118
|
+
isValidating.value = true
|
|
119
|
+
validateFields(true)
|
|
120
|
+
return errors.value.length === 0
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
defineExpose({
|
|
124
|
+
validateOnSubmit,
|
|
125
|
+
})
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<template>
|
|
129
|
+
<VTextField
|
|
130
|
+
v-model="password"
|
|
131
|
+
:error-messages="errors"
|
|
132
|
+
:messages="successes"
|
|
133
|
+
:type="showEyeIcon ? 'text' : 'password'"
|
|
134
|
+
:variant="outlined ? 'outlined' : 'underlined'"
|
|
135
|
+
class="vd-password"
|
|
136
|
+
color="primary"
|
|
137
|
+
title="password"
|
|
138
|
+
validate-on="blur lazy"
|
|
139
|
+
:class="{
|
|
140
|
+
'v-messages__message--success': successes.length > 0
|
|
141
|
+
}"
|
|
142
|
+
@blur="validateFields(true)"
|
|
143
|
+
@keydown="handleKeydown"
|
|
144
|
+
@update:model-value="emitChangeEvent"
|
|
145
|
+
>
|
|
146
|
+
<template #append-inner>
|
|
147
|
+
<VBtn
|
|
148
|
+
:aria-label="btnLabel"
|
|
149
|
+
class="mx-auto"
|
|
150
|
+
v-bind="options.btn"
|
|
151
|
+
@click="showEyeIcon = !showEyeIcon"
|
|
152
|
+
>
|
|
153
|
+
<VIcon v-bind="options.icon">
|
|
154
|
+
{{ showEyeIcon ? eyeIcon : eyeOffIcon }}
|
|
155
|
+
</VIcon>
|
|
156
|
+
</VBtn>
|
|
157
|
+
</template>
|
|
158
|
+
</VTextField>
|
|
159
|
+
</template>
|
|
160
|
+
|
|
161
|
+
<style lang="scss" scoped>
|
|
162
|
+
@use '@/assets/tokens';
|
|
163
|
+
|
|
164
|
+
.vd-password {
|
|
165
|
+
.v-btn--icon.v-btn--density-default {
|
|
166
|
+
width: var(--v-btn-height);
|
|
167
|
+
height: var(--v-btn-height);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
:deep(.v-field.v-field--variant-underlined .v-field__append-inner) {
|
|
171
|
+
padding-top: 0;
|
|
172
|
+
padding-bottom: 0;
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
:deep(.v-field.v-field--variant-underlined .v-field__input) {
|
|
178
|
+
padding-top: calc(var(--v-field-input-padding-top) - 15px);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.v-messages__message--success {
|
|
183
|
+
color: tokens.$colors-border-success !important;
|
|
184
|
+
|
|
185
|
+
.v-field--active & {
|
|
186
|
+
color: tokens.$colors-border-success !important;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
</style>
|