@ouestfrance/sipa-bms-ui 8.20.0 → 8.22.0

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 (46) hide show
  1. package/dist/components/feedback/BmsGhost.vue.d.ts +24 -0
  2. package/dist/components/form/BmsAutocomplete.vue.d.ts +2 -2
  3. package/dist/components/form/BmsFilePicker.vue.d.ts +4 -0
  4. package/dist/components/form/BmsMultiSelect.vue.d.ts +4 -0
  5. package/dist/components/form/BmsServerAutocomplete.vue.d.ts +2 -2
  6. package/dist/components/form/RawAutocomplete.vue.d.ts +11 -3
  7. package/dist/components/layout/BmsFloatingWindow.vue.d.ts +20 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/mockServiceWorker.js +1 -1
  10. package/dist/sipa-bms-ui.css +117 -393
  11. package/dist/sipa-bms-ui.es.js +447 -129
  12. package/dist/sipa-bms-ui.es.js.map +1 -1
  13. package/dist/sipa-bms-ui.umd.js +449 -130
  14. package/dist/sipa-bms-ui.umd.js.map +1 -1
  15. package/package.json +12 -12
  16. package/src/assets/scss/_conf.scss +0 -1
  17. package/src/assets/scss/app.scss +0 -1
  18. package/src/components/button/BmsAllButtons.stories.js +50 -23
  19. package/src/components/button/BmsButton.stories.js +153 -59
  20. package/src/components/feedback/BmsGhost.stories.js +60 -0
  21. package/src/components/feedback/BmsGhost.vue +82 -0
  22. package/src/components/feedback/BmsTooltip.vue +15 -1
  23. package/src/components/feedback/UiTooltip.vue +9 -1
  24. package/src/components/form/BmsFilePicker.stories.js +6 -1
  25. package/src/components/form/BmsFilePicker.vue +10 -5
  26. package/src/components/form/BmsMultiSelect.vue +32 -25
  27. package/src/components/form/BmsSelect.vue +18 -16
  28. package/src/components/form/RawAutocomplete.vue +16 -4
  29. package/src/components/form/RawInputText.vue +1 -0
  30. package/src/components/layout/BmsFloatingWindow.stories.js +226 -12
  31. package/src/components/layout/BmsFloatingWindow.vue +286 -13
  32. package/src/components/layout/BmsModal.stories.js +2 -1
  33. package/src/components/layout/BmsSplitWindow.vue +0 -1
  34. package/src/components/table/BmsTableFilters.vue +1 -1
  35. package/src/documentation/button/primaryButton.mdx +142 -0
  36. package/src/documentation/{secondaryButton.mdx → button/secondaryButton.mdx} +2 -2
  37. package/src/documentation/foundation/contributing.mdx +72 -0
  38. package/src/documentation/foundation/gettingstarted.mdx +7 -0
  39. package/src/documentation/{principles.mdx → foundation/principles.mdx} +9 -9
  40. package/src/documentation/icons.mdx +43 -0
  41. package/src/index.ts +3 -0
  42. package/src/showroom/pages/forms.vue +10 -1
  43. package/src/assets/scss/_formkit.scss +0 -353
  44. package/src/components/form/Form.stories.js +0 -35
  45. package/src/documentation/primaryButton.mdx +0 -20
  46. /package/src/documentation/{button.mdx → button/button.mdx} +0 -0
@@ -5,7 +5,7 @@
5
5
  :model-value="modelValue"
6
6
  :open="isDatalistOpen"
7
7
  @select="onSelect"
8
- @click="setFocus"
8
+ @click="onSelectClick"
9
9
  >
10
10
  <template #input>
11
11
  <div class="tags">
@@ -24,11 +24,8 @@
24
24
  v-model="searching"
25
25
  class="search"
26
26
  :disabled="disabled"
27
- @focus="openDatalist"
28
- @click="openDatalist"
27
+ @input="onInput"
29
28
  @keyup.down="openDatalist"
30
- @input="openDatalist"
31
- @keyup.backspace="onBackspace"
32
29
  />
33
30
  </div>
34
31
 
@@ -37,16 +34,8 @@
37
34
  <X class="icon icon-clear" @click.stop="clearInput" />
38
35
  </template>
39
36
  <template v-else>
40
- <ChevronUp
41
- v-if="isDatalistOpen"
42
- class="icon icon-toggle-button"
43
- @click="closeDatalist"
44
- />
45
- <ChevronDown
46
- v-else
47
- class="icon icon-toggle-button"
48
- @click="openDatalist"
49
- />
37
+ <ChevronUp v-if="isDatalistOpen" class="icon icon-toggle-button" />
38
+ <ChevronDown v-else class="icon icon-toggle-button" />
50
39
  </template>
51
40
  </span>
52
41
  </template>
@@ -67,7 +56,7 @@ import { InputOption } from '@/models';
67
56
  import { searchString } from '@/helpers';
68
57
  import { FieldComponentProps } from '@/plugins/field/field-component.model';
69
58
  import RawSelect from './RawSelect.vue';
70
- import { onClickOutside } from '@vueuse/core';
59
+ import { onClickOutside, onKeyDown, onKeyUp } from '@vueuse/core';
71
60
 
72
61
  export interface Props extends FieldComponentProps {
73
62
  options: InputOption[] | string[];
@@ -80,9 +69,16 @@ const props = withDefaults(defineProps<Props>(), {
80
69
  required: false,
81
70
  });
82
71
 
83
- const inputElement: Ref<HTMLElement | null> = ref(null);
72
+ const emits = defineEmits<{
73
+ select: [option: InputOption | string];
74
+ input: [e: InputEvent];
75
+ }>();
76
+
77
+ const modelValue = defineModel<string[] | null>('modelValue', { default: [] });
84
78
 
79
+ const inputElement: Ref<HTMLElement | null> = ref(null);
85
80
  const isDatalistOpen = ref(false);
81
+ const searching = ref('');
86
82
 
87
83
  const closeDatalist = () => (isDatalistOpen.value = false);
88
84
  const openDatalist = () => {
@@ -91,13 +87,9 @@ const openDatalist = () => {
91
87
  }
92
88
  };
93
89
 
94
- const searching = ref('');
95
-
96
- const modelValue = defineModel<string[] | null>('modelValue', { default: [] });
97
-
98
- onClickOutside(inputElement, closeDatalist, {
99
- ignore: ['.datalist-option', '.icon-toggle-button'],
100
- });
90
+ const onBlur = () => {
91
+ closeDatalist();
92
+ };
101
93
 
102
94
  const onBackspace = () => {
103
95
  if (
@@ -107,6 +99,21 @@ const onBackspace = () => {
107
99
  )
108
100
  modelValue.value.splice(-1);
109
101
  };
102
+ onClickOutside(inputElement, closeDatalist, {
103
+ ignore: ['.datalist-option', '.icon-toggle-button'],
104
+ });
105
+ onKeyUp('Escape', closeDatalist);
106
+ onKeyUp('Tab', closeDatalist);
107
+
108
+ onKeyDown('Backspace', onBackspace);
109
+
110
+ const onSelectClick = () => {
111
+ isDatalistOpen.value = !isDatalistOpen.value;
112
+ };
113
+ const onInput = (e: InputEvent) => {
114
+ emits('input', e);
115
+ openDatalist();
116
+ };
110
117
 
111
118
  const setFocus = () => {
112
119
  if (inputElement.value) {
@@ -122,8 +129,8 @@ const onSelect = (option: InputOption | string) => {
122
129
  }
123
130
 
124
131
  searching.value = '';
132
+ emits('select', option);
125
133
  setFocus();
126
- closeDatalist();
127
134
  };
128
135
 
129
136
  const removeOption = (value: string) => {
@@ -3,7 +3,7 @@
3
3
  v-bind="$props"
4
4
  :open="isDatalistOpen"
5
5
  @select="onSelect"
6
- @click="setFocus"
6
+ @click="onSelectClick"
7
7
  >
8
8
  <template #input>
9
9
  <input
@@ -15,17 +15,11 @@
15
15
  :placeholder="placeholder"
16
16
  :required="required"
17
17
  :disabled="disabled"
18
- @focus="openDatalist"
19
- @click="openDatalist"
20
18
  @keyup.down="openDatalist"
21
19
  />
22
20
  <span class="icon-toggle-container">
23
- <ChevronUp
24
- v-if="isDatalistOpen"
25
- class="icon-toggle-button"
26
- @click="closeDatalist"
27
- />
28
- <ChevronDown v-else class="icon-toggle-button" @click="openDatalist" />
21
+ <ChevronUp v-if="isDatalistOpen" class="icon-toggle-button" />
22
+ <ChevronDown v-else class="icon-toggle-button" />
29
23
  </span>
30
24
  </template>
31
25
  </RawSelect>
@@ -33,12 +27,12 @@
33
27
 
34
28
  <script lang="ts" setup>
35
29
  import { ChevronDown, ChevronUp } from 'lucide-vue-next';
36
- import { computed, Ref, ref } from 'vue';
30
+ import { computed, Ref, ref, watch } from 'vue';
37
31
  import _ from 'lodash';
38
32
  import { InputOption } from '@/models';
39
33
  import { FieldComponentProps } from '@/plugins/field/field-component.model';
40
34
  import RawSelect from './RawSelect.vue';
41
- import { onClickOutside } from '@vueuse/core';
35
+ import { onClickOutside, onKeyUp } from '@vueuse/core';
42
36
 
43
37
  export interface Props extends FieldComponentProps {
44
38
  options: InputOption[];
@@ -55,10 +49,13 @@ const props = withDefaults(defineProps<Props>(), {
55
49
  open: false,
56
50
  });
57
51
 
52
+ const emits = defineEmits<{
53
+ select: [value: any];
54
+ }>();
55
+
58
56
  const modelValue = defineModel<any>('modelValue', { required: true });
59
57
 
60
58
  const inputElement: Ref<HTMLElement | null> = ref(null);
61
-
62
59
  const isDatalistOpen = ref(props.open);
63
60
 
64
61
  const closeDatalist = () => {
@@ -71,13 +68,11 @@ const openDatalist = () => {
71
68
  }
72
69
  };
73
70
 
74
- const emits = defineEmits<{
75
- select: [value: any];
76
- }>();
77
-
78
71
  onClickOutside(inputElement, closeDatalist, {
79
72
  ignore: ['.datalist-option', '.icon-toggle-button'],
80
73
  });
74
+ onKeyUp('Escape', closeDatalist);
75
+ onKeyUp('Tab', closeDatalist);
81
76
 
82
77
  const displayValue = computed(() => {
83
78
  const option = props.options.find((o) =>
@@ -100,6 +95,13 @@ const onSelect = (option: any) => {
100
95
  closeDatalist();
101
96
  };
102
97
 
98
+ const onSelectClick = () => {
99
+ isDatalistOpen.value = !isDatalistOpen.value;
100
+ if (isDatalistOpen) {
101
+ setFocus();
102
+ }
103
+ };
104
+
103
105
  defineExpose({
104
106
  setFocus,
105
107
  });
@@ -10,7 +10,8 @@
10
10
  :required="required"
11
11
  :small="small"
12
12
  @input="onInput"
13
- @focus="onFocus"
13
+ @click="onClick"
14
+ @keyup.down="openDatalist"
14
15
  >
15
16
  <template #icon-start>
16
17
  <slot name="icon-start"></slot>
@@ -58,7 +59,7 @@ import RawInputText from '@/components/form/RawInputText.vue';
58
59
  import { InputOption, InputType } from '@/models';
59
60
  import { ChevronDown, ChevronUp, X } from 'lucide-vue-next';
60
61
  import { FieldComponentProps } from '@/plugins/field/field-component.model';
61
- import { MaybeElementRef, onClickOutside } from '@vueuse/core';
62
+ import { MaybeElementRef, onClickOutside, onKeyUp } from '@vueuse/core';
62
63
 
63
64
  export interface Props extends FieldComponentProps {
64
65
  options: InputOption[];
@@ -82,6 +83,10 @@ const rawInput: Ref<HTMLElement | null> = ref(null);
82
83
  const emits = defineEmits<{
83
84
  addNewOption: [newOption: string];
84
85
  select: [option: InputOption];
86
+ blur: [];
87
+ focus: [];
88
+ click: [];
89
+ input: [e: InputEvent];
85
90
  }>();
86
91
 
87
92
  const getValidOptionByLabel = (label: string) =>
@@ -102,6 +107,13 @@ onClickOutside(rawInput as MaybeElementRef, closeDatalist, {
102
107
  ignore: ['.datalist-option', '.icon-toggle-button', '.icon-clear'],
103
108
  });
104
109
 
110
+ const onBlur = () => {
111
+ emits('blur');
112
+ closeDatalist();
113
+ };
114
+ onKeyUp('Escape', onBlur);
115
+ onKeyUp('Tab', onBlur);
116
+
105
117
  const classes = computed(() => {
106
118
  return { 'is-error': props.errors?.length, 'is-disabled': props.disabled };
107
119
  });
@@ -148,8 +160,8 @@ watch(
148
160
  },
149
161
  );
150
162
 
151
- const onFocus = () => {
152
- openDatalist();
163
+ const onClick = () => {
164
+ isDatalistOpen.value = !isDatalistOpen.value;
153
165
  };
154
166
 
155
167
  const onInput = () => {
@@ -21,6 +21,7 @@
21
21
  @blur="$emits('blur')"
22
22
  @input="onInput"
23
23
  @focus="$emits('focus')"
24
+ @click="$emits('click')"
24
25
  />
25
26
  <span class="field__input-icon field__input-icon--end">
26
27
  <slot name="icon-end"></slot>
@@ -14,7 +14,7 @@ const Template = (args) => ({
14
14
  return { args };
15
15
  },
16
16
  template: `
17
- <div style="width: ${args.width}; height: ${args.height}; background: purple; position:relative">
17
+ <div style="width: ${args.containerWidth || '1200px'}; height: ${args.containerHeight || '100vh'}; background: purple; position:relative">
18
18
  <BmsFloatingWindow v-bind="args">
19
19
  ${args?.content}
20
20
  </BmsFloatingWindow>
@@ -24,32 +24,31 @@ const Template = (args) => ({
24
24
 
25
25
  export const Default = Template.bind({});
26
26
  Default.args = {
27
- width: '200px',
28
- height: '200px',
29
27
  title: 'My title',
30
28
  modelValue: true,
31
29
  };
32
30
 
33
31
  export const Large = Template.bind({});
34
32
  Large.args = {
33
+ containerWidth: '1200px',
34
+ containerHeight: '100vh',
35
35
  width: '1200px',
36
- height: '100vh',
37
36
  title: 'My title',
38
37
  modelValue: true,
39
38
  };
40
39
 
41
40
  export const Closed = Template.bind({});
42
41
  Closed.args = {
43
- width: '1200px',
44
- height: '100vh',
42
+ containerWidth: '1200px',
43
+ containerHeight: '100vh',
45
44
  title: 'My title',
46
45
  modelValue: false,
47
46
  };
48
47
 
49
48
  export const Overflow = Template.bind({});
50
49
  Overflow.args = {
51
- width: '1200px',
52
- height: '100vh',
50
+ containerWidth: '1200px',
51
+ containerHeight: '100vh',
53
52
  title: 'Cyrano de Bergerac',
54
53
  modelValue: true,
55
54
  content: `Ah ! non ! c’est un peu court, jeune homme !
@@ -99,11 +98,226 @@ Si vous aviez un peu de lettres et d’esprit
99
98
  Mais d’esprit, ô le plus lamentable des êtres,
100
99
  Vous n’en eûtes jamais un atome, et de lettres
101
100
  Vous n’avez que les trois qui forment le mot : sot !
102
- Eussiez-vous eu, dailleurs, linvention quil faut
101
+ Eussiez-vous eu, d'ailleurs, l'invention qu'il faut
103
102
  Pour pouvoir là, devant ces nobles galeries,
104
103
  me servir toutes ces folles plaisanteries,
105
- Que vous nen eussiez pas articulé le quart
106
- De la moitié du commencement dune, car
104
+ Que vous n'en eussiez pas articulé le quart
105
+ De la moitié du commencement d'une, car
107
106
  Je me les sers moi-même, avec assez de verve,
108
- Mais je ne permets pas quun autre me les serve.`,
107
+ Mais je ne permets pas qu'un autre me les serve.`,
108
+ };
109
+
110
+ const IframeTemplate = (args) => ({
111
+ components: {
112
+ BmsFloatingWindow,
113
+ },
114
+ setup() {
115
+ return { args };
116
+ },
117
+ template: `
118
+ <div style="width: 100%; height: 100vh; position: relative;">
119
+ <iframe
120
+ src="https://example.com"
121
+ style="width: 100%; height: 100%; border: none;"
122
+ ></iframe>
123
+ <BmsFloatingWindow v-bind="args">
124
+ <p style="padding: 1em;">Essayez de déplacer cette fenêtre au-dessus de l'iframe.</p>
125
+ <p style="padding: 1em;">Le drag and drop fonctionne grâce à l'overlay qui intercepte les événements.</p>
126
+ </BmsFloatingWindow>
127
+ </div>
128
+ `,
129
+ });
130
+
131
+ export const WithIframe = IframeTemplate.bind({});
132
+ WithIframe.args = {
133
+ title: 'Drag me over the iframe',
134
+ modelValue: true,
135
+ };
136
+ WithIframe.parameters = {
137
+ docs: {
138
+ description: {
139
+ story:
140
+ "Démontre le drag and drop fonctionnant au-dessus d'une iframe. L'overlay transparent capture les événements souris même quand la souris passe au-dessus de l'iframe.",
141
+ },
142
+ },
143
+ };
144
+
145
+ export const PlacementTopLeft = Template.bind({});
146
+ PlacementTopLeft.args = {
147
+ containerWidth: '1200px',
148
+ containerHeight: '100vh',
149
+ title: 'Top Left Placement',
150
+ modelValue: true,
151
+ defaultPlacement: 'top-left',
152
+ };
153
+ PlacementTopLeft.parameters = {
154
+ docs: {
155
+ description: {
156
+ story: 'Fenêtre positionnée par défaut en haut à gauche.',
157
+ },
158
+ },
159
+ };
160
+
161
+ export const PlacementTopRight = Template.bind({});
162
+ PlacementTopRight.args = {
163
+ containerWidth: '1200px',
164
+ containerHeight: '100vh',
165
+ title: 'Top Right Placement',
166
+ modelValue: true,
167
+ defaultPlacement: 'top-right',
168
+ };
169
+
170
+ export const PlacementBottomLeft = Template.bind({});
171
+ PlacementBottomLeft.args = {
172
+ containerWidth: '1200px',
173
+ containerHeight: '100vh',
174
+ title: 'Bottom Left Placement',
175
+ modelValue: true,
176
+ defaultPlacement: 'bottom-left',
177
+ };
178
+
179
+ export const PlacementBottomRight = Template.bind({});
180
+ PlacementBottomRight.args = {
181
+ containerWidth: '1200px',
182
+ containerHeight: '100vh',
183
+ title: 'Bottom Right Placement',
184
+ modelValue: true,
185
+ defaultPlacement: 'bottom-right',
186
+ };
187
+
188
+ export const Expandable = Template.bind({});
189
+ Expandable.args = {
190
+ containerWidth: '1200px',
191
+ containerHeight: '100vh',
192
+ title: 'Fenêtre expandable',
193
+ modelValue: true,
194
+ expandable: true,
195
+ content:
196
+ '<p style="padding: 1em;">Cliquez sur l\'icône maximize pour agrandir la fenêtre au parent.</p>',
197
+ };
198
+ Expandable.parameters = {
199
+ docs: {
200
+ description: {
201
+ story:
202
+ "Fenêtre avec bouton expand activé. Cliquez sur l'icône pour agrandir/réduire.",
203
+ },
204
+ },
205
+ };
206
+
207
+ export const ExpandableCustomSize = Template.bind({});
208
+ ExpandableCustomSize.args = {
209
+ containerWidth: '1200px',
210
+ containerHeight: '100vh',
211
+ title: 'Expand avec dimensions custom',
212
+ modelValue: true,
213
+ expandable: true,
214
+ expandedWidth: '80%',
215
+ expandedHeight: '80%',
216
+ content:
217
+ '<p style="padding: 1em;">Expand avec dimensions personnalisées (80% x 80%).</p>',
218
+ };
219
+ ExpandableCustomSize.parameters = {
220
+ docs: {
221
+ description: {
222
+ story:
223
+ 'Fenêtre expandable avec dimensions personnalisées (expandedWidth et expandedHeight).',
224
+ },
225
+ },
226
+ };
227
+
228
+ const NestedContainersTemplate = (args) => ({
229
+ components: {
230
+ BmsFloatingWindow,
231
+ },
232
+ setup() {
233
+ return { args };
234
+ },
235
+ template: `
236
+ <div id="outer-container"
237
+ style="width: 100%; height: 100vh; background-color: #667eea; padding: 2em; box-sizing: border-box;">
238
+ <p style="color: white; margin: 0 0 1em;">Container externe - id="outer-container"</p>
239
+ <div id="inner-container"
240
+ style="width: 80%; height: 80%; background-color: #f093fb; padding: 2em; box-sizing: border-box; position: relative;">
241
+ <p style="color: white; margin: 0 0 1em;">Container interne - id="inner-container"</p>
242
+ <BmsFloatingWindow v-bind="args">
243
+ <p style="padding: 1em;">Cliquez sur expand pour voir la fenêtre s'agrandir sur le container
244
+ <strong>interne</strong>.</p>
245
+ <p style="padding: 0 1em;">La prop <code>expandTarget="#inner-container"</code> définit la cible.</p>
246
+ </BmsFloatingWindow>
247
+ </div>
248
+ </div>
249
+ `,
250
+ });
251
+
252
+ export const ExpandToInnerTarget = NestedContainersTemplate.bind({});
253
+ ExpandToInnerTarget.args = {
254
+ title: 'Expand vers inner container',
255
+ modelValue: true,
256
+ expandable: true,
257
+ expandTarget: '#inner-container',
258
+ defaultPlacement: 'top-left',
259
+ };
260
+ ExpandToInnerTarget.parameters = {
261
+ docs: {
262
+ description: {
263
+ story:
264
+ "Démontre expandTarget : la fenêtre s'agrandit sur le container interne (rose) plutôt que sur son parent direct.",
265
+ },
266
+ },
267
+ };
268
+
269
+ export const ExpandToOuterTarget = NestedContainersTemplate.bind({});
270
+ ExpandToOuterTarget.args = {
271
+ title: 'Expand vers outer container',
272
+ modelValue: true,
273
+ expandable: true,
274
+ expandTarget: '#outer-container',
275
+ defaultPlacement: 'top-left',
276
+ };
277
+ ExpandToOuterTarget.parameters = {
278
+ docs: {
279
+ description: {
280
+ story:
281
+ "Démontre expandTarget : la fenêtre s'agrandit sur le container externe (violet).",
282
+ },
283
+ },
284
+ };
285
+
286
+ const ForeignContainersTemplate = (args) => ({
287
+ components: {
288
+ BmsFloatingWindow,
289
+ },
290
+ setup() {
291
+ return { args };
292
+ },
293
+ template: `
294
+ <div id="foreign-container" style="width: 80%; height: 80%; background-color: green; padding: 2em; box-sizing: border-box;">
295
+ <p style="color: white; margin: 0 0 1em;">Container interne - id="foreign-container"</p>
296
+ </div>
297
+ <p style="color: white; margin: 0 0 1em;">Container externe - id="outer-container"</p>
298
+ <div id="inner-container" style="width: 80%; height: 80%; background-color: #f093fb; padding: 2em; box-sizing: border-box; ">
299
+ <p style="color: white; margin: 0 0 1em;">Container interne - id="inner-container"</p>
300
+ <BmsFloatingWindow v-bind="args">
301
+ <p style="padding: 1em;">Cliquez sur expand pour voir la fenêtre s'agrandir sur le container <strong>interne</strong>.</p>
302
+ <p style="padding: 0 1em;">La prop <code>expandTarget="#inner-container"</code> définit la cible.</p>
303
+ </BmsFloatingWindow>
304
+ </div>
305
+ `,
306
+ });
307
+
308
+ export const ForeignContainers = ForeignContainersTemplate.bind({});
309
+ ForeignContainers.args = {
310
+ title: 'Expand vers outer foreign',
311
+ modelValue: true,
312
+ expandable: true,
313
+ expandTarget: '#foreign-container',
314
+ defaultPlacement: 'top-left',
315
+ };
316
+ ForeignContainers.parameters = {
317
+ docs: {
318
+ description: {
319
+ story:
320
+ "Démontre expandTarget : la fenêtre s'agrandit sur le container extérieur à son arbre (violet).",
321
+ },
322
+ },
109
323
  };