@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.
Files changed (122) hide show
  1. package/dist/design-system-v3.d.ts +331 -372
  2. package/dist/design-system-v3.js +2794 -2637
  3. package/dist/design-system-v3.umd.cjs +1 -10
  4. package/dist/style.css +1 -1
  5. package/package.json +10 -2
  6. package/src/assets/settings.scss +2 -2
  7. package/src/assets/tokens.scss +107 -112
  8. package/src/components/BackBtn/BackBtn.stories.ts +4 -1
  9. package/src/components/BackBtn/BackBtn.vue +4 -4
  10. package/src/components/BackToTopBtn/BackToTopBtn.stories.ts +3 -3
  11. package/src/components/BackToTopBtn/BackToTopBtn.vue +1 -0
  12. package/src/components/CollapsibleList/CollapsibleList.mdx +1 -1
  13. package/src/components/CollapsibleList/CollapsibleList.vue +43 -44
  14. package/src/components/ContextualMenu/ContextualMenu.mdx +118 -0
  15. package/src/components/ContextualMenu/ContextualMenu.stories.ts +430 -0
  16. package/src/components/ContextualMenu/ContextualMenu.vue +101 -0
  17. package/src/components/ContextualMenu/tests/ContextualMenu.spec.ts +115 -0
  18. package/src/components/ContextualMenu/tests/__snapshots__/ContextualMenu.spec.ts.snap +10 -0
  19. package/src/components/ContextualMenu/types.ts +5 -0
  20. package/src/components/CookieBanner/CookieBanner.stories.ts +3 -2
  21. package/src/components/CookieBanner/CookieBanner.vue +13 -10
  22. package/src/components/CookieBanner/tests/__snapshots__/CookieBanner.spec.ts.snap +17 -15
  23. package/src/components/CookiesSelection/CookiesInformation/CookiesInformation.vue +6 -1
  24. package/src/components/CookiesSelection/CookiesInformation/locales.ts +1 -0
  25. package/src/components/CookiesSelection/CookiesTable/CookiesTable.vue +1 -0
  26. package/src/components/CookiesSelection/tests/__snapshots__/CookiesSelection.spec.ts.snap +17 -15
  27. package/src/components/CopyBtn/CopyBtn.stories.ts +5 -2
  28. package/src/components/CopyBtn/CopyBtn.vue +7 -7
  29. package/src/components/Customs/SyBtnSelect/SyBtnSelect.stories.ts +11 -51
  30. package/src/components/Customs/SyBtnSelect/SyBtnSelect.vue +26 -26
  31. package/src/components/Customs/SyInputSelect/SyInputSelect.mdx +4 -1
  32. package/src/components/Customs/SyInputSelect/SyInputSelect.stories.ts +19 -7
  33. package/src/components/Customs/SyInputSelect/SyInputSelect.vue +24 -24
  34. package/src/components/Customs/SySelect/SySelect.mdx +1 -2
  35. package/src/components/Customs/SySelect/SySelect.stories.ts +0 -2
  36. package/src/components/Customs/SySelect/SySelect.vue +27 -26
  37. package/src/components/DataList/DataList.stories.ts +3 -2
  38. package/src/components/DataList/DataList.vue +1 -1
  39. package/src/components/DataListGroup/DataListGroup.stories.ts +3 -2
  40. package/src/components/DataListItem/DataListItem.vue +12 -12
  41. package/src/components/DialogBox/DialogBox.mdx +28 -2
  42. package/src/components/DialogBox/DialogBox.stories.ts +1 -1
  43. package/src/components/DialogBox/DialogBox.vue +3 -2
  44. package/src/components/DownloadBtn/DownloadBtn.mdx +3 -4
  45. package/src/components/DownloadBtn/DownloadBtn.stories.ts +20 -21
  46. package/src/components/DownloadBtn/DownloadBtn.vue +2 -1
  47. package/src/components/ErrorPage/ErrorPage.vue +1 -1
  48. package/src/components/ExternalLinks/ExternalLinks.mdx +86 -0
  49. package/src/components/ExternalLinks/ExternalLinks.stories.ts +553 -0
  50. package/src/components/ExternalLinks/ExternalLinks.vue +200 -0
  51. package/src/components/ExternalLinks/config.ts +34 -0
  52. package/src/components/ExternalLinks/locales.ts +4 -0
  53. package/src/components/ExternalLinks/tests/ExternalLinks.spec.ts +154 -0
  54. package/src/components/ExternalLinks/tests/__snapshots__/ExternalLinks.spec.ts.snap +159 -0
  55. package/src/components/FooterBar/FooterBar.vue +111 -82
  56. package/src/components/FranceConnectBtn/FranceConnectBtn.stories.ts +2 -1
  57. package/src/components/FranceConnectBtn/FranceConnectBtn.vue +14 -13
  58. package/src/components/HeaderBar/HeaderBar.stories.ts +19 -12
  59. package/src/components/HeaderBar/HeaderBar.vue +8 -3
  60. package/src/components/HeaderBar/HeaderBurgerMenu/HeaderBurgerMenu.vue +12 -7
  61. package/src/components/HeaderBar/HeaderBurgerMenu/HeaderMenuItem/HeaderMenuItem.vue +5 -5
  62. package/src/components/HeaderBar/HeaderBurgerMenu/HeaderMenuSection/HeaderMenuSection.vue +2 -2
  63. package/src/components/HeaderBar/HeaderBurgerMenu/HeaderSubMenu/HeaderSubMenu.vue +10 -8
  64. package/src/components/HeaderBar/HeaderLogo/HeaderLogo.vue +2 -2
  65. package/src/components/HeaderBar/HeaderLogo/logos/Logo-mobile.vue +2 -1
  66. package/src/components/HeaderBar/HeaderLogo/logos/Logo.vue +2 -1
  67. package/src/components/HeaderBar/HeaderMenuBtn/HeaderMenuBtn.vue +10 -10
  68. package/src/components/HeaderBar/consts.scss +1 -1
  69. package/src/components/HeaderLoading/HeaderLoading.vue +12 -11
  70. package/src/components/HeaderNavigationBar/HeaderNavigationBar.stories.ts +104 -32
  71. package/src/components/HeaderNavigationBar/HeaderNavigationBar.vue +2 -1
  72. package/src/components/HeaderNavigationBar/HorizontalNavbar/HorizontalNavbar.vue +9 -9
  73. package/src/components/HeaderToolbar/HeaderToolbar.vue +215 -180
  74. package/src/components/LangBtn/LangBtn.vue +8 -6
  75. package/src/components/LogoBrandSection/LogoBrandSection.stories.ts +2 -2
  76. package/src/components/NirField/NirField.mdx +1 -4
  77. package/src/components/NirField/NirField.stories.ts +71 -18
  78. package/src/components/NirField/NirField.vue +49 -49
  79. package/src/components/NirField/tests/NirField.spec.ts +1 -0
  80. package/src/components/NotFoundPage/NotFoundPage.stories.ts +33 -2
  81. package/src/components/NotFoundPage/NotFoundPage.vue +17 -0
  82. package/src/components/NotificationBar/NotificationBar.mdx +5 -5
  83. package/src/components/NotificationBar/NotificationBar.stories.ts +410 -314
  84. package/src/components/NotificationBar/NotificationBar.vue +43 -41
  85. package/src/components/PageContainer/PageContainer.stories.ts +5 -5
  86. package/src/components/PageContainer/PageContainer.vue +13 -8
  87. package/src/components/PageContainer/tests/PageContainer.spec.ts +1 -1
  88. package/src/components/PasswordField/PasswordField.mdx +70 -0
  89. package/src/components/PasswordField/PasswordField.stories.ts +213 -0
  90. package/src/components/PasswordField/PasswordField.vue +189 -0
  91. package/src/components/PasswordField/config.ts +11 -0
  92. package/src/components/PasswordField/locales.ts +4 -0
  93. package/src/components/PasswordField/tests/PasswordField.spec.ts +96 -0
  94. package/src/components/PhoneField/PhoneField.mdx +0 -2
  95. package/src/components/PhoneField/PhoneField.stories.ts +10 -50
  96. package/src/components/PhoneField/PhoneField.vue +34 -34
  97. package/src/components/SkipLink/SkipLink.vue +10 -10
  98. package/src/components/SocialMediaLinks/SocialMediaLinks.vue +29 -21
  99. package/src/components/SocialMediaLinks/tests/__snapshots__/SocialMediaLinks.spec.ts.snap +2 -2
  100. package/src/components/SubHeader/SubHeader.stories.ts +6 -3
  101. package/src/components/SubHeader/SubHeader.vue +32 -31
  102. package/src/components/SyAlert/SyAlert.vue +15 -8
  103. package/src/components/UserMenuBtn/UserMenuBtn.vue +1 -1
  104. package/src/components/UserMenuBtn/config.ts +1 -1
  105. package/src/components/index.ts +10 -6
  106. package/src/designTokens/index.ts +6 -4
  107. package/src/designTokens/{bootstrapColors.md → paColors.md} +1 -1
  108. package/src/designTokens/tokens/cnam/cnamLightTheme.ts +2 -0
  109. package/src/designTokens/tokens/pa/paColors.ts +171 -0
  110. package/src/designTokens/tokens/pa/paContextual.ts +58 -0
  111. package/src/designTokens/tokens/pa/paDarkTheme.ts +5 -0
  112. package/src/designTokens/tokens/pa/paLightTheme.ts +123 -0
  113. package/src/designTokens/tokens/pa/paSemantic.ts +87 -0
  114. package/src/stories/GuideDuDev/CreerUneIssue.mdx +64 -0
  115. package/src/stories/GuideDuDev/{CommentUtiliserLesRules.mdx → UtiliserLesRules.mdx} +2 -2
  116. package/src/stories/GuideDuDev/components.stories.ts +9 -7
  117. package/src/stories/Guidelines/Vuetify/Vuetify.stories.ts +163 -88
  118. package/src/stories/Guidelines/Vuetify/VuetifyItems.ts +250 -23
  119. package/src/temp/TestDTComponent.vue +5 -6
  120. package/src/designTokens/tokens/bootstrap/bootstrapColors.ts +0 -158
  121. package/src/designTokens/tokens/bootstrap/bootstrapLightTheme.ts +0 -22
  122. 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-longText' : 'action-section-shortText'"
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.scss';
231
+ @use '@/assets/tokens';
232
232
 
233
233
  .vd-notification-content {
234
- display: flex;
235
- align-items: center;
234
+ display: flex;
235
+ align-items: center;
236
236
  }
237
237
 
238
238
  .vd-notification-bar :deep(.v-snack__wrapper) {
239
- padding: 16px;
240
- min-width: 0;
241
- max-width: none;
239
+ padding: 16px;
240
+ min-width: 0;
241
+ max-width: none;
242
242
  }
243
243
 
244
244
  :deep(.v-snackbar__content) {
245
- padding: tokens.$padding-4 !important;
245
+ padding: tokens.$padding-4 !important;
246
246
  }
247
+
247
248
  :deep(.v-snackbar__actions) {
248
- margin-inline-end: 10px;
249
+ margin-inline-end: 10px;
249
250
  }
250
251
 
251
252
  .vd-notification-bar.v-snackbar--vertical :deep() {
252
- .v-snackbar--vertical .v-snackbar__wrapper .v-snackbar__actions {
253
- width: 100% !important;
254
- align-self: auto;
255
- }
256
- .v-snack__wrapper {
257
- align-items: stretch;
258
- flex-direction: row;
259
- }
260
-
261
- .v-snack__action {
262
- align-self: stretch;
263
- align-items: stretch;
264
- flex-direction: column;
265
- }
266
-
267
- .v-snackbar__content {
268
- margin: 0;
269
- width: 100%;
270
- display: flex;
271
- }
272
-
273
- .vd-notification-content {
274
- flex-direction: column;
275
- display: flex;
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
- width: 98% !important;
282
+ width: 98% !important;
281
283
  }
282
284
 
283
285
  .short-text :deep(.v-snackbar__actions) {
284
- width: 48% !important;
286
+ width: 48% !important;
285
287
  }
286
288
 
287
- .action-section-longText {
288
- justify-content: space-around;
289
+ .action-section-long-text {
290
+ justify-content: space-around;
289
291
  }
290
292
 
291
- .action-section-shortText {
292
- justify-content: end !important;
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', 'l', 'm', 's'],
14
+ options: ['xl', 'lg', 'md', 'sm', 'xs'],
15
15
  control: { type: 'select' },
16
- default: 'xl',
16
+ default: undefined,
17
17
  },
18
18
  spacing: {
19
- options: ['xs', 'sm', 'md', 'lg', 'xl'],
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="s">
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: 's',
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' | 'l' | 'm' | 's'
7
- spacing?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
6
+ size?: 'xl' | 'lg' | 'md' | 'sm' | 'xs'
7
+ spacing?: 'xl' | 'lg' | 'md' | 'sm' | 'xs'
8
8
  color?: string
9
9
  }>(), {
10
- size: 'xl',
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
- return sizeMapping[display.name.value] ?? sizeMapping[props.size ?? 'xl']
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
- flex: 1;
64
- width: 100%;
65
- max-width: 1712px;
66
- margin: 0 auto;
68
+ flex: 1;
69
+ width: 100%;
70
+ max-width: 1712px;
71
+ margin: 0 auto;
67
72
  }
68
73
  </style>
@@ -44,7 +44,7 @@ describe('PageContainer', () => {
44
44
  it('containerSize computed property', async () => {
45
45
  const wrapper = mount(PageContainer, {
46
46
  props: {
47
- size: 'l',
47
+ size: 'md',
48
48
  },
49
49
  global: {
50
50
  plugins: [vuetify],
@@ -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>
@@ -0,0 +1,11 @@
1
+ import { cnamColorsTokens } from '@/designTokens/tokens/cnam/cnamColors'
2
+
3
+ export const config = {
4
+ btn: {
5
+ variant: 'text',
6
+ icon: true,
7
+ },
8
+ icon: {
9
+ color: cnamColorsTokens.grey.lighten20,
10
+ },
11
+ } as const
@@ -0,0 +1,4 @@
1
+ export const locales = {
2
+ hidePassword: 'Masquer le mot de passe',
3
+ showPassword: 'Afficher le mot de passe',
4
+ }