@cnamts/synapse 0.0.9-alpha → 0.0.10-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 (55) hide show
  1. package/dist/design-system-v3.d.ts +631 -62
  2. package/dist/design-system-v3.js +3451 -2650
  3. package/dist/design-system-v3.umd.cjs +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/components/DatePicker/Accessibilite.mdx +14 -0
  7. package/src/components/DatePicker/Accessibilite.stories.ts +191 -0
  8. package/src/components/DatePicker/AccessibiliteItems.ts +233 -0
  9. package/src/components/DatePicker/DatePicker.mdx +1 -6
  10. package/src/components/DatePicker/DatePicker.stories.ts +16 -16
  11. package/src/components/DatePicker/DatePicker.vue +20 -6
  12. package/src/components/DatePicker/constants/ExpertiseLevelEnum.ts +4 -0
  13. package/src/components/FileList/FileList.mdx +103 -0
  14. package/src/components/FileList/FileList.stories.ts +562 -0
  15. package/src/components/FileList/FileList.vue +78 -0
  16. package/src/components/FileList/UploadItem/UploadItem.vue +270 -0
  17. package/src/components/FileList/UploadItem/locales.ts +9 -0
  18. package/src/components/FileList/tests/FileList.spec.ts +176 -0
  19. package/src/components/FilePreview/FilePreview.mdx +82 -0
  20. package/src/components/FilePreview/FilePreview.stories.ts +242 -0
  21. package/src/components/FilePreview/FilePreview.vue +68 -0
  22. package/src/components/FilePreview/config.ts +10 -0
  23. package/src/components/FilePreview/locales.ts +4 -0
  24. package/src/components/FilePreview/tests/FilePreview.spec.ts +124 -0
  25. package/src/components/FilePreview/tests/__snapshots__/FilePreview.spec.ts.snap +11 -0
  26. package/src/components/PeriodField/PeriodField.mdx +32 -0
  27. package/src/components/PeriodField/PeriodField.stories.ts +807 -0
  28. package/src/components/PeriodField/PeriodField.vue +355 -0
  29. package/src/components/PeriodField/tests/PeriodField.spec.ts +348 -0
  30. package/src/components/RangeField/Accessibilite.mdx +14 -0
  31. package/src/components/RangeField/Accessibilite.stories.ts +191 -0
  32. package/src/components/RangeField/AccessibiliteItems.ts +179 -0
  33. package/src/components/RangeField/constants/ExpertiseLevelEnum.ts +4 -0
  34. package/src/components/RatingPicker/Accessibilite.mdx +14 -0
  35. package/src/components/RatingPicker/Accessibilite.stories.ts +191 -0
  36. package/src/components/RatingPicker/AccessibiliteItems.ts +208 -0
  37. package/src/components/RatingPicker/constants/ExpertiseLevelEnum.ts +4 -0
  38. package/src/components/SearchListField/Accessibilite.mdx +14 -0
  39. package/src/components/SearchListField/Accessibilite.stories.ts +191 -0
  40. package/src/components/SearchListField/AccessibiliteItems.ts +310 -0
  41. package/src/components/SearchListField/constants/ExpertiseLevelEnum.ts +4 -0
  42. package/src/components/SelectBtnField/Accessibilite.mdx +14 -0
  43. package/src/components/SelectBtnField/Accessibilite.stories.ts +191 -0
  44. package/src/components/SelectBtnField/AccessibiliteItems.ts +191 -0
  45. package/src/components/SelectBtnField/constants/ExpertiseLevelEnum.ts +4 -0
  46. package/src/components/SyAlert/SyAlert.vue +11 -9
  47. package/src/components/TableToolbar/TableToolbar.mdx +130 -0
  48. package/src/components/TableToolbar/TableToolbar.stories.ts +935 -0
  49. package/src/components/TableToolbar/TableToolbar.vue +168 -0
  50. package/src/components/TableToolbar/config.ts +24 -0
  51. package/src/components/TableToolbar/locales.ts +6 -0
  52. package/src/components/TableToolbar/tests/TableToolbar.spec.ts +166 -0
  53. package/src/components/TableToolbar/tests/__snapshots__/TableToolbar.spec.ts.snap +359 -0
  54. package/src/components/index.ts +3 -0
  55. package/src/composables/rules/useFieldValidation.ts +17 -15
@@ -0,0 +1,78 @@
1
+ <script setup lang="ts">
2
+ import UploadItem from '@/components/FileList/UploadItem/UploadItem.vue'
3
+ import { useWidthable, type Widthable } from '@/composables/widthable'
4
+ import { locales as defaultLocales } from './UploadItem/locales'
5
+
6
+ export interface Item {
7
+ id: string
8
+ title: string
9
+ state: string // 'initial' | 'success' | 'error' | 'loading'
10
+ fileName?: string
11
+ optional?: boolean
12
+ progress?: number
13
+ showUploadBtn?: boolean
14
+ showPreviewBtn?: boolean
15
+ showDeleteBtn?: boolean
16
+ }
17
+
18
+ const props = withDefaults(defineProps<{
19
+ uploadList: Item[]
20
+ locales?: typeof defaultLocales
21
+ } & Widthable>(), {
22
+ locales: () => defaultLocales,
23
+ })
24
+
25
+ const { widthStyles } = useWidthable(props)
26
+
27
+ defineEmits<{
28
+ (e: 'upload', item: Item): void
29
+ (e: 'preview', item: Item): void
30
+ (e: 'delete', item: Item): void
31
+ }>()
32
+ </script>
33
+
34
+ <template>
35
+ <ul
36
+ class="upload-list"
37
+ :style="widthStyles"
38
+ >
39
+ <UploadItem
40
+ v-for="item in props.uploadList"
41
+ :key="item.id"
42
+ :item-id="item.id"
43
+ :title="item.title"
44
+ :file-name="item.fileName"
45
+ :optional="item.optional"
46
+ :state="(item.state as 'initial' | 'success' | 'error' | 'loading')"
47
+ :progress="item.progress"
48
+ :show-upload-btn="item.showUploadBtn"
49
+ :show-preview-btn="item.showPreviewBtn"
50
+ :show-delete-btn="item.showDeleteBtn"
51
+ tag="li"
52
+ :locales="locales"
53
+ @upload="() => $emit('upload', uploadList.find((i) => i.id === item.id) as Item)"
54
+ @preview="() => $emit('preview', uploadList.find((i) => i.id === item.id) as Item)"
55
+ @delete="() => $emit('delete', uploadList.find((i) => i.id === item.id) as Item)"
56
+ >
57
+ <template #file-icon="slotProps">
58
+ <slot
59
+ :name="`file-icon-${item.id}`"
60
+ v-bind="slotProps"
61
+ />
62
+ </template>
63
+ </UploadItem>
64
+ </ul>
65
+ </template>
66
+
67
+ <style lang="scss" scoped>
68
+ @use '@/assets/tokens';
69
+
70
+ .upload-list {
71
+ display: flex;
72
+ flex-direction: column;
73
+ margin: 0;
74
+ padding: 0;
75
+ list-style: none;
76
+ }
77
+
78
+ </style>
@@ -0,0 +1,270 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ mdiFile,
4
+ mdiTrayArrowUp,
5
+ mdiDeleteOutline,
6
+ mdiEyeOutline,
7
+ mdiAlertCircle,
8
+ mdiCheckCircle,
9
+ } from '@mdi/js'
10
+ import { cnamContextualTokens } from '@/designTokens/tokens/cnam/cnamContextual'
11
+ import { locales as defaultLocales } from './locales'
12
+
13
+ type FileState = 'initial' | 'success' | 'error' | 'loading'
14
+
15
+ defineEmits<{
16
+ (e: 'upload', item: string): void
17
+ (e: 'preview', item: string): void
18
+ (e: 'delete', item: string): void
19
+ }>()
20
+
21
+ withDefaults(defineProps<{
22
+ itemId: string
23
+ title: string
24
+ fileName?: string
25
+ message?: string
26
+ optional?: boolean
27
+ state?: FileState
28
+ progress?: number
29
+ showUploadBtn?: boolean
30
+ showDeleteBtn?: boolean
31
+ showPreviewBtn?: boolean
32
+ tag?: string
33
+ locales?: typeof defaultLocales
34
+ }>(), {
35
+ fileName: undefined,
36
+ message: undefined,
37
+ optional: false,
38
+ state: 'initial',
39
+ progress: undefined,
40
+ showUploadBtn: true,
41
+ showDeleteBtn: true,
42
+ showPreviewBtn: false,
43
+ tag: 'div',
44
+ locales: () => defaultLocales,
45
+ })
46
+
47
+ defineSlots<{
48
+ 'file-icon'(props: { state: FileState }): void
49
+ }>()
50
+
51
+ </script>
52
+
53
+ <template>
54
+ <component
55
+ :is="tag"
56
+ class="file-item"
57
+ >
58
+ <div class="file-item__description">
59
+ <div class="file-item__content">
60
+ <span
61
+ class="file-item__icon"
62
+ >
63
+ <slot
64
+ name="file-icon"
65
+ :state
66
+ >
67
+ <span
68
+ v-if="state === 'success'"
69
+ class="d-sr-only"
70
+ >
71
+ {{ locales.success }}
72
+ </span>
73
+
74
+ <span
75
+ v-else-if="state === 'error'"
76
+ class="d-sr-only"
77
+ >
78
+ {{ locales.error }}
79
+ </span>
80
+
81
+ <VIcon
82
+ v-if="state === 'error'"
83
+ :size="cnamContextualTokens.iconSize.default"
84
+ :aria-label="locales.error"
85
+ color="error"
86
+ >{{ mdiAlertCircle }}</VIcon>
87
+
88
+ <VIcon
89
+ v-else-if="state === 'success'"
90
+ :size="cnamContextualTokens.iconSize.default"
91
+ :aria-label="locales.success"
92
+ color="success"
93
+ >{{ mdiCheckCircle }}</VIcon>
94
+
95
+ <VIcon
96
+ v-else
97
+ :size="cnamContextualTokens.iconSize.default"
98
+ color="primary"
99
+ >{{ mdiFile }}</VIcon>
100
+ </slot>
101
+ </span>
102
+ <div>
103
+ <div class="file-item__title">
104
+ {{ title }}
105
+ </div>
106
+ <div class="file-item__name text-base">
107
+ {{ fileName }}
108
+ </div>
109
+ <div
110
+ v-if="message || optional"
111
+ class="file-item__message text-base"
112
+ >
113
+ {{ message ?? locales.optionalDocument }}
114
+ </div>
115
+ </div>
116
+ </div>
117
+ <div class="file-item__actions">
118
+ <VBtn
119
+ v-if="(state === 'initial' || state == 'error') && showUploadBtn"
120
+ class="file-item__action file-item__action-upload text-primary"
121
+ variant="text"
122
+ @click="$emit('upload', itemId)"
123
+ >
124
+ <span>Importer</span>
125
+ <template #prepend>
126
+ <VIcon
127
+ color="primary"
128
+ >
129
+ {{ mdiTrayArrowUp }}
130
+ </VIcon>
131
+ </template>
132
+ </VBtn>
133
+ <VBtn
134
+ v-if="state === 'success' && showPreviewBtn"
135
+ class="file-item__action file-item__action-preview text-primary"
136
+ variant="text"
137
+ @click="$emit('preview', itemId)"
138
+ >
139
+ <span>{{ locales.see }}</span>
140
+ <template #prepend>
141
+ <VIcon
142
+ color="primary"
143
+ >
144
+ {{ mdiEyeOutline }}
145
+ </VIcon>
146
+ </template>
147
+ </VBtn>
148
+ <VBtn
149
+ v-if="state === 'success' && showDeleteBtn"
150
+ class="file-item__action file-item__action-delete text-error"
151
+ variant="text"
152
+ @click="$emit('delete', itemId)"
153
+ >
154
+ <span>{{ locales.delete }}</span>
155
+ <template #prepend>
156
+ <VIcon
157
+ color="error"
158
+ >
159
+ {{ mdiDeleteOutline }}
160
+ </VIcon>
161
+ </template>
162
+ </VBtn>
163
+ </div>
164
+ </div>
165
+
166
+ <div
167
+ v-if="state === 'loading'"
168
+ class="file-item__message-progress"
169
+ >
170
+ <p class="d-sr-only">
171
+ {{ locales.uploading }}
172
+ </p>
173
+ <VProgressLinear
174
+ :indeterminate="progress === undefined"
175
+ :model-value="progress"
176
+ :progress="progress"
177
+ height="7"
178
+ color="primary"
179
+ rounded="true"
180
+ />
181
+ </div>
182
+ </component>
183
+ </template>
184
+
185
+ <style lang="scss" scoped>
186
+ @use '@/assets/tokens';
187
+
188
+ .file-item {
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: tokens.$gap-3;
192
+ padding-block: tokens.$padding-4;
193
+ border-bottom: 1px solid tokens.$colors-border-subdued;
194
+ }
195
+
196
+ .file-item__title {
197
+ font-size: tokens.$font-size-body-text;
198
+ }
199
+
200
+ .file-item__name {
201
+ font-size: 0.875rem;
202
+ color: tokens.$colors-text-base;
203
+ }
204
+
205
+ .file-item__description {
206
+ display: flex;
207
+ justify-content: space-between;
208
+ align-items: center;
209
+ flex-wrap: wrap;
210
+
211
+ > * {
212
+ grid-column: 1 / -1;
213
+ }
214
+
215
+ > *:nth-child(1),
216
+ > *:nth-child(2) {
217
+ grid-column: 1 / 2;
218
+ }
219
+
220
+ > .file-item__actions {
221
+ grid-column-start: 2;
222
+ grid-row: span 2;
223
+ }
224
+ }
225
+
226
+ .file-item__content {
227
+ display: flex;
228
+ gap: tokens.$gap-4;
229
+ align-items: center;
230
+ }
231
+
232
+ .file-item__actions {
233
+ display: flex;
234
+ flex-direction: column;
235
+ align-items: end;
236
+ justify-content: center;
237
+ margin-left: auto;
238
+ height: 100%;
239
+ gap: tokens.$gap-1;
240
+
241
+ @media screen and (min-width: tokens.$container-tablet-max-width) {
242
+ flex-direction: row;
243
+ }
244
+ }
245
+
246
+ .file-item__action {
247
+ display: flex;
248
+ justify-content: end;
249
+ text-transform: unset;
250
+ padding: 0.625rem 1.25rem;
251
+ font-weight: bold;
252
+ }
253
+
254
+ .file-item__message {
255
+ font-size: 0.875rem;
256
+ color: tokens.$colors-text-subdued;
257
+ }
258
+
259
+ .file-item__message-success,
260
+ .file-item__message-error {
261
+ margin-top: tokens.$gap-3;
262
+ }
263
+
264
+ .file-item__message-error {
265
+ display: flex;
266
+ align-items: center;
267
+ gap: tokens.$gap-4;
268
+ }
269
+
270
+ </style>
@@ -0,0 +1,9 @@
1
+ export const locales = {
2
+ optionalDocument: 'Document facultatif',
3
+ see: 'Voir',
4
+ delete: 'Supprimer',
5
+ uploading: 'En cours',
6
+ success: 'Téléchargé',
7
+ error: 'Erreur',
8
+ errorOccurred: 'Une erreur est survenue pendant le téléchargement.',
9
+ }
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import FileList from '../FileList.vue'
4
+ import { vuetify } from '@tests/unit/setup'
5
+ import { locales } from '../UploadItem/locales'
6
+
7
+ describe('FileList', () => {
8
+ it('renders many file items', () => {
9
+ const wrapper = mount(FileList, {
10
+ props: {
11
+ uploadList: [
12
+ {
13
+ id: 'residenceCertificate',
14
+ title: 'Attestation de domicile',
15
+ state: 'initial',
16
+ },
17
+ {
18
+ id: 'identityCard',
19
+ title: 'Carte d\'identité',
20
+ state: 'initial',
21
+ },
22
+ ],
23
+ },
24
+ global: {
25
+ plugins: [vuetify],
26
+ },
27
+ })
28
+ expect(wrapper.findAll('.file-item').length).toBe(2)
29
+ const item1 = wrapper.findAll('.file-item').at(0)
30
+ expect(item1!.text()).toContain('Attestation de domicile')
31
+ const item2 = wrapper.findAll('.file-item').at(1)
32
+ expect(item2!.text()).toContain('Carte d\'identité')
33
+ })
34
+
35
+ it('shows the right action for each state and item preference', async () => {
36
+ const wrapper = mount(FileList, {
37
+ props: {
38
+ uploadList: [
39
+ {
40
+ id: 'file1',
41
+ title: 'file1',
42
+ state: 'initial',
43
+ },
44
+ {
45
+ id: 'file2',
46
+ title: 'file2',
47
+ state: 'initial',
48
+ showUploadBtn: false,
49
+ },
50
+ {
51
+ id: 'file3',
52
+ title: 'file3',
53
+ state: 'success',
54
+ showDeleteBtn: true,
55
+ showPreviewBtn: true,
56
+ },
57
+ {
58
+ id: 'file4',
59
+ title: 'file4',
60
+ state: 'success',
61
+ showDeleteBtn: false,
62
+ showPreviewBtn: false,
63
+ },
64
+ {
65
+ id: 'file5',
66
+ title: 'file5',
67
+ state: 'error',
68
+ },
69
+ {
70
+ id: 'file6',
71
+ title: 'file6',
72
+ state: 'error',
73
+ showUploadBtn: false,
74
+ },
75
+ ],
76
+ },
77
+ global: {
78
+ plugins: [vuetify],
79
+ },
80
+ })
81
+
82
+ const item1 = wrapper.findAll('.file-item').at(0)
83
+ expect(item1!.findAll('button').length).toBe(1)
84
+ const item1UploadBtn = item1!.find('.file-item__action-upload')
85
+ expect(item1UploadBtn.exists()).toBe(true)
86
+
87
+ const item2 = wrapper.findAll('.file-item').at(1)
88
+ expect(item2!.findAll('button').length).toBe(0)
89
+
90
+ const item3 = wrapper.findAll('.file-item').at(2)
91
+ expect(item3!.findAll('button').length).toBe(2)
92
+ const item3DeleteBtn = item3!.find('.file-item__action-delete')
93
+ expect(item3DeleteBtn.exists()).toBe(true)
94
+ const item3PreviewBtn = item3!.find('.file-item__action-preview')
95
+ expect(item3PreviewBtn.exists()).toBe(true)
96
+
97
+ const item4 = wrapper.findAll('.file-item').at(3)
98
+ expect(item4!.findAll('button').length).toBe(0)
99
+
100
+ const item5 = wrapper.findAll('.file-item').at(4)
101
+ expect(item5!.findAll('button').length).toBe(1)
102
+ const item5UploadBtn = item5!.find('.file-item__action-upload')
103
+ expect(item5UploadBtn.exists()).toBe(true)
104
+
105
+ const item6 = wrapper.findAll('.file-item').at(5)
106
+ expect(item6!.findAll('button').length).toBe(0)
107
+ })
108
+
109
+ it('emits the right event when clicking on an action button', async () => {
110
+ const fileItem1 = {
111
+ id: 'file1',
112
+ title: 'file1',
113
+ state: 'initial',
114
+ }
115
+
116
+ const fileItem2 = {
117
+ id: 'file2',
118
+ title: 'file2',
119
+ state: 'success',
120
+ showDeleteBtn: true,
121
+ showPreviewBtn: true,
122
+ }
123
+
124
+ const wrapper = mount(FileList, {
125
+ props: {
126
+ uploadList: [fileItem1, fileItem2],
127
+ },
128
+ global: {
129
+ plugins: [vuetify],
130
+ },
131
+ })
132
+
133
+ const item1 = wrapper.findAll('.file-item').at(0)
134
+ const item1UploadBtn = item1!.find('.file-item__action-upload')
135
+ await item1UploadBtn!.trigger('click')
136
+ expect(wrapper.emitted('upload')?.[0][0]).toEqual(fileItem1)
137
+
138
+ const item2 = wrapper.findAll('.file-item').at(1)
139
+ const item2DeleteBtn = item2!.find('.file-item__action-delete')
140
+ await item2DeleteBtn!.trigger('click')
141
+ expect(wrapper.emitted('delete')?.[0][0]).toEqual(fileItem2)
142
+
143
+ const item2PreviewBtn = item2!.find('.file-item__action-preview')
144
+ await item2PreviewBtn!.trigger('click')
145
+ expect(wrapper.emitted('preview')?.[0][0]).toEqual(fileItem2)
146
+ })
147
+
148
+ it('shows when a file is optional', () => {
149
+ const wrapper = mount(FileList, {
150
+ props: {
151
+ uploadList: [
152
+ {
153
+ id: 'file1',
154
+ title: 'file1',
155
+ state: 'initial',
156
+ optional: true,
157
+ },
158
+ {
159
+ id: 'file2',
160
+ title: 'file2',
161
+ state: 'initial',
162
+ },
163
+ ],
164
+ },
165
+ global: {
166
+ plugins: [vuetify],
167
+ },
168
+ })
169
+
170
+ const item1 = wrapper.findAll('.file-item').at(0)
171
+ expect(item1!.text()).toContain(locales.optionalDocument)
172
+
173
+ const item2 = wrapper.findAll('.file-item').at(1)
174
+ expect(item2!.text()).not.toContain(locales.optionalDocument)
175
+ })
176
+ })
@@ -0,0 +1,82 @@
1
+ import {Controls, Canvas, Meta, Source} from '@storybook/blocks';
2
+
3
+ import * as FilePreviewStories from './FilePreview.stories.ts'
4
+
5
+ <Meta of={FilePreviewStories} />
6
+
7
+
8
+ # FilePreview
9
+
10
+ L'élément `FilePreview` est utilisé pour afficher l'aperçu d'un fichier.
11
+
12
+ <Canvas of={FilePreviewStories.Default} />
13
+
14
+
15
+ # API
16
+
17
+ <Controls of={FilePreviewStories.Default} />
18
+
19
+
20
+ # Exemple
21
+
22
+ ## Afficher un fichier depuis une API
23
+
24
+ Vous pouvez afficher une image ou un fichier PDF récupéré depuis une API sous forme de `Blob`.
25
+
26
+ <Canvas
27
+ of={FilePreviewStories.FromApi}
28
+ source={{
29
+ language: 'html',
30
+ format: 'dedent',
31
+ code: `
32
+ <script lang="ts" setup>
33
+ import { onMounted, ref } from 'vue'
34
+ import { FilePreview } from '@cnamts/synapse'
35
+
36
+ const file = ref<File | Blob | undefined>()
37
+
38
+ onMounted(() => {
39
+ fetch('https://picsum.photos/seed/picsum/750/350')
40
+ .then(res => res.blob())
41
+ .then(blob => file.value = blob)
42
+ })
43
+ </script>
44
+
45
+ <template>
46
+ <FilePreview :file="file" />
47
+ </template>
48
+ `,
49
+ }}
50
+ />
51
+
52
+ ## Fichier non supporté
53
+
54
+ Lorsque le fichier n'est pas supporté, un message d'erreur est affiché.
55
+
56
+ <Canvas of={FilePreviewStories.UnsupportedFile} />
57
+
58
+ ## Usage avec `FileUpload`
59
+
60
+ Vous pouvez utiliser ce composant en combinaison avec `FileUpload` pour afficher un aperçu du fichier avant de l'envoyer.
61
+
62
+ <Canvas
63
+ of={FilePreviewStories.WithFileUpload}
64
+ source={{
65
+ language: 'html',
66
+ format: 'dedent',
67
+ code: `
68
+ <script lang="ts" setup>
69
+ import { ref } from 'vue'
70
+ import { FilePreview, FileUpload } from '@cnamts/synapse'
71
+
72
+ const files = ref<File[]>([])
73
+ </script>
74
+ <template>
75
+ <div>
76
+ <FileUpload v-model="files" class="mb-4"/>
77
+ <FilePreview :file="files[0]"/>
78
+ </div>
79
+ </template>
80
+ `,
81
+ }}
82
+ />