@ouestfrance/sipa-bms-ui 8.16.0 → 8.17.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouestfrance/sipa-bms-ui",
3
- "version": "8.16.0",
3
+ "version": "8.17.0",
4
4
  "author": "Ouest-France BMS",
5
5
  "license": "ISC",
6
6
  "scripts": {
@@ -10,6 +10,12 @@ export default {
10
10
  disableSearch: {
11
11
  control: { type: 'boolean' },
12
12
  },
13
+ selectMode: {
14
+ options: ['single', 'default'],
15
+ },
16
+ mode: {
17
+ options: ['normal', 'dense'],
18
+ },
13
19
  loading: {
14
20
  control: { type: 'boolean' },
15
21
  },
@@ -751,6 +757,47 @@ Selected.args = {
751
757
  selectable: true,
752
758
  };
753
759
 
760
+ export const SingleSelected = Template.bind({});
761
+ SingleSelected.args = {
762
+ headers: [
763
+ {
764
+ label: 'Column 1',
765
+ key: 'col1',
766
+ align: 'start',
767
+ },
768
+ {
769
+ label: 'Column 2',
770
+ key: 'col2',
771
+ align: 'center',
772
+ },
773
+ {
774
+ label: 'Column 3',
775
+ key: 'col3',
776
+ align: 'end',
777
+ },
778
+ ],
779
+ items: [
780
+ {
781
+ col1: 'Value1',
782
+ col2: 'Value2',
783
+ col3: 'Value3',
784
+ },
785
+ {
786
+ col1: 'Value4',
787
+ col2: 'Value5',
788
+ col3: 'Value6',
789
+ },
790
+ ],
791
+ selectedItems: [
792
+ {
793
+ col1: 'Value1',
794
+ col2: 'Value2',
795
+ col3: 'Value3',
796
+ },
797
+ ],
798
+ selectable: true,
799
+ selectMode: 'single',
800
+ };
754
801
  const TemplateWithSelectAction = (args) => ({
755
802
  components: {
756
803
  BmsTable,
@@ -6,7 +6,14 @@ import UiFilterButton from '@/components/table/UiFilterButton.vue';
6
6
  import { usePagination } from '@/composables/pagination.composable';
7
7
  import { useSearch } from '@/composables/search.composable';
8
8
  import { useSort } from '@/composables/sort.composable';
9
- import { Filter, SavedFilter, Sort, SortValue, TableHeader } from '@/models';
9
+ import {
10
+ Filter,
11
+ SavedFilter,
12
+ SelectMode,
13
+ Sort,
14
+ SortValue,
15
+ TableHeader,
16
+ } from '@/models';
10
17
  import { computed, Ref, ref, watch, watchEffect } from 'vue';
11
18
  import BmsTableFilters from './BmsTableFilters.vue';
12
19
  import BmsSearch from '../form/BmsSearch.vue';
@@ -34,6 +41,7 @@ interface UiTableProps {
34
41
  defaultSort?: Sort;
35
42
  selectable?: boolean;
36
43
  selectableDisabled?: boolean;
44
+ selectMode?: SelectMode.DEFAULT | SelectMode.SINGLE;
37
45
  }
38
46
 
39
47
  const props = withDefaults(defineProps<UiTableProps>(), {
@@ -51,6 +59,7 @@ const props = withDefaults(defineProps<UiTableProps>(), {
51
59
  }),
52
60
  selectable: false,
53
61
  selectableDisabled: false,
62
+ selectMode: SelectMode.DEFAULT,
54
63
  });
55
64
 
56
65
  const {
@@ -212,9 +221,18 @@ const onClickHeader = (header: TableHeader) => {
212
221
  changeSort(header);
213
222
  };
214
223
 
224
+ const elementsAndChildElements = computed(() => {
225
+ const childElements = items.value
226
+ .map((item) => item.childElement)
227
+ .filter((childElement) => !!childElement);
228
+ return [...items.value, ...childElements];
229
+ });
230
+
215
231
  const onSelectAll = () => {
216
- selectedItems.value = items.value;
232
+ selectedItems.value = elementsAndChildElements.value;
217
233
  };
234
+
235
+ const totalSize = computed(() => elementsAndChildElements.value.length);
218
236
  </script>
219
237
 
220
238
  <template>
@@ -228,7 +246,8 @@ const onSelectAll = () => {
228
246
  :sort="sort"
229
247
  :selectable="selectable"
230
248
  :selectableDisabled="selectableDisabled"
231
- :totalSize="items.length"
249
+ :totalSize="totalSize"
250
+ :selectMode="selectMode"
232
251
  @clickHeader="onClickHeader"
233
252
  @selectAll="onSelectAll"
234
253
  >
@@ -2,9 +2,8 @@ import UiBmsTable from '@/components/table/UiBmsTable.vue';
2
2
  import { field } from '@/plugins/field';
3
3
  import { mount, MountingOptions } from '@vue/test-utils';
4
4
  import UiBmsInputCheckbox from '../form/UiBmsInputCheckbox.vue';
5
- import BmsAlert from '../feedback/BmsAlert.vue';
6
- import { wrap } from 'lodash';
7
5
  import { SelectMode } from '@/models';
6
+ import BmsInputRadio from '../form/BmsInputRadio.vue';
8
7
 
9
8
  const intersectionObserverMock = () => ({
10
9
  observe: () => null,
@@ -40,13 +39,21 @@ const factory = (options: MountingOptions<any, {}> = {}) => {
40
39
  } as any);
41
40
  return {
42
41
  wrapper,
42
+ selectedMessage: () => wrapper.findAll('.bms-table__selected')[0],
43
+
44
+ getAllCheckboxes: () => wrapper.findAllComponents(UiBmsInputCheckbox),
43
45
  getAllCheckedCheckboxes: () =>
44
46
  wrapper
45
47
  .findAllComponents(UiBmsInputCheckbox)
46
48
  .filter((checkbox) => checkbox.props('modelValue') === true),
47
49
  getTableCheckbox: () => wrapper.findAllComponents(UiBmsInputCheckbox)[0],
48
50
  getFirstRowCheckbox: () => wrapper.findAllComponents(UiBmsInputCheckbox)[1],
49
- selectedMessage: () => wrapper.findAll('.bms-table__selected')[0],
51
+
52
+ getAllCheckedRadio: () =>
53
+ wrapper
54
+ .findAllComponents(BmsInputRadio)
55
+ .filter((checkbox) => checkbox.props('modelValue') === true),
56
+ getAllRadio: () => wrapper.findAllComponents(BmsInputRadio),
50
57
  };
51
58
  };
52
59
  describe('UiBmsTable', () => {
@@ -369,5 +376,42 @@ describe('UiBmsTable', () => {
369
376
  'Vous avez sélectionné la totalité des 2 éléments.',
370
377
  );
371
378
  });
379
+ it('should display readio if in selectMode SINGLE', async () => {
380
+ const { getAllRadio, getAllCheckboxes } = factory({
381
+ props: {
382
+ headers,
383
+ items,
384
+ totalSize: 2,
385
+ selectable: true,
386
+ selectMode: SelectMode.SINGLE,
387
+ selectedItems: [items[0]],
388
+ },
389
+ });
390
+
391
+ expect(getAllRadio().length).toEqual(2);
392
+ expect(getAllCheckboxes().length).toEqual(0);
393
+ });
394
+ it('should unselect previous selection if in selectMode SINGLE', async () => {
395
+ const { getAllRadio } = factory({
396
+ props: {
397
+ headers,
398
+ items,
399
+ totalSize: 2,
400
+ selectable: true,
401
+ selectMode: SelectMode.SINGLE,
402
+ selectedItems: [items[0]],
403
+ },
404
+ });
405
+ const firstRadio = getAllRadio()[0];
406
+ const secondRadio = getAllRadio()[1];
407
+
408
+ expect(firstRadio.props('modelValue')).toBeTruthy();
409
+ expect(secondRadio.props('modelValue')).toBeFalsy();
410
+
411
+ await secondRadio.get('input').setValue(items[1]);
412
+
413
+ expect(secondRadio.props('modelValue')).toBeTruthy();
414
+ expect(firstRadio.props('modelValue')).toBeFalsy();
415
+ });
372
416
  });
373
417
  });
@@ -559,6 +559,49 @@ SelectedWithMaxSelected.args = {
559
559
  maxSelectedSize: 1,
560
560
  };
561
561
 
562
+ export const SingleSelect = Template.bind({});
563
+ SingleSelect.args = {
564
+ headers: [
565
+ {
566
+ label: 'Column 1',
567
+ key: 'col1',
568
+ align: 'start',
569
+ },
570
+ {
571
+ label: 'Column 2',
572
+ key: 'col2',
573
+ align: 'center',
574
+ },
575
+ {
576
+ label: 'Column 3',
577
+ key: 'col3',
578
+ align: 'end',
579
+ },
580
+ ],
581
+ items: [
582
+ {
583
+ col1: 'Value1',
584
+ col2: 'Value2',
585
+ col3: 'Value3',
586
+ },
587
+ {
588
+ col1: 'Value4',
589
+ col2: 'Value5',
590
+ col3: 'Value6',
591
+ },
592
+ ],
593
+ selectedItems: [
594
+ {
595
+ col1: 'Value1',
596
+ col2: 'Value2',
597
+ col3: 'Value3',
598
+ },
599
+ ],
600
+ selectable: true,
601
+ selectMode: 'single',
602
+ totalSize: 2,
603
+ };
604
+
562
605
  export const AllSelected = Template.bind({});
563
606
  AllSelected.args = {
564
607
  headers: [
@@ -21,12 +21,12 @@ import BmsLoader from '../feedback/BmsLoader.vue';
21
21
  import { ChevronDown, ChevronsUpDown, ChevronUp } from 'lucide-vue-next';
22
22
  import UiBmsInputCheckbox from '../form/UiBmsInputCheckbox.vue';
23
23
  import BmsAlert from '../feedback/BmsAlert.vue';
24
- import { enforceActionsColumnHeader } from '@/helpers';
24
+ import { enforceActionsColumnHeader, getHeaderClasses } from '@/helpers';
25
25
  import UiBmsTableRow from './UiBmsTableRow.vue';
26
26
 
27
27
  interface UiBmsTableProps {
28
28
  headers: TableHeader[];
29
- items: unknown[];
29
+ items: any[];
30
30
  mode?: 'normal' | 'dense';
31
31
  loading?: boolean;
32
32
  hasFilters?: boolean;
@@ -62,37 +62,31 @@ const emits = defineEmits<{
62
62
  clearSelection: [];
63
63
  }>();
64
64
 
65
+ // Pagination
65
66
  const pagination = ref<HTMLInputElement | null>(null);
66
- const isHeaderStuck = ref<boolean>(false);
67
- const thead = ref<HTMLHeadElement | null>(null);
67
+ const isFocusOnPagination = () =>
68
+ pagination.value?.contains(document.activeElement);
68
69
 
69
- const filteredHeaders = computed(() => {
70
- return enforceActionsColumnHeader(props.headers);
71
- });
70
+ watch(
71
+ () => props.items,
72
+ async function keepFocusOnPaginationWhenUsed(newVal, oldVal) {
73
+ if (oldVal.length > 0 && newVal.length > 0 && isFocusOnPagination()) {
74
+ await nextTick();
75
+ pagination.value?.focus();
76
+ }
77
+ },
78
+ { deep: true },
79
+ );
80
+
81
+ // Headers style
82
+ const filteredHeaders = computed(() =>
83
+ enforceActionsColumnHeader(props.headers),
84
+ );
72
85
 
73
86
  const tableClass = computed(
74
87
  () => `bms-table__table bms-table__table--${props.mode}`,
75
88
  );
76
89
 
77
- const getAlignClass = (header: TableHeader) => {
78
- const align = !header.align ? 'start' : header.align;
79
- return `u-text-align-${align}`;
80
- };
81
-
82
- const getHeaderClasses = (header: TableHeader): string[] => {
83
- const classes = [getAlignClass(header), 'bms-table__header-cell'];
84
- if (header.class) {
85
- classes.push(header.class);
86
- }
87
- if (header.sortable) {
88
- classes.push('sortable');
89
- }
90
- if (props.sort.key === header.key) {
91
- classes.push('sorted');
92
- }
93
- return classes;
94
- };
95
-
96
90
  const getSortComponent = (header: TableHeader): Component => {
97
91
  if (!props.sort.key || header.key !== props.sort.key) {
98
92
  return ChevronsUpDown;
@@ -107,48 +101,23 @@ const getSortComponent = (header: TableHeader): Component => {
107
101
  }
108
102
  };
109
103
 
110
- const isFocusOnPagination = () => {
111
- return pagination.value?.contains(document.activeElement);
112
- };
113
-
114
- watch(
115
- () => props.items,
116
- async (newVal, oldVal) => {
117
- if (oldVal.length > 0 && newVal.length > 0 && isFocusOnPagination()) {
118
- await nextTick();
119
- pagination.value?.focus();
120
- }
121
- },
122
- { deep: true },
123
- );
124
-
125
- onscroll = () => {
126
- if (thead.value) {
127
- const { top: theadTop } = thead.value.getBoundingClientRect();
128
- const headers = document.getElementsByTagName('header');
129
- if (headers.length > 0) {
130
- const header = headers[0];
131
- const { height: headerHeight } = header.getBoundingClientRect();
132
- isHeaderStuck.value = headerHeight === theadTop;
133
- }
134
- }
135
- };
136
-
137
- const isItemSelected = (item: unknown): boolean => {
138
- return (
139
- props.selectMode === SelectMode.ALL ||
140
- !!selectedItems.value.find((it) => _isEqual(item, it))
141
- );
142
- };
104
+ // Selection
105
+ const isItemSelected = (item: unknown): boolean =>
106
+ props.selectMode === SelectMode.ALL ||
107
+ !!selectedItems.value.find((it) => _isEqual(item, it));
143
108
 
144
109
  const onItemSelect = (item: unknown) => {
145
- if (isItemSelected(item)) {
146
- selectedItems.value = selectedItems.value.filter(
147
- (it) => !_isEqual(item, it),
148
- );
149
- areAllCurrentItemsSelected.value = false;
110
+ if (props.selectMode === SelectMode.SINGLE) {
111
+ selectedItems.value = [item];
150
112
  } else {
151
- selectedItems.value.push(item);
113
+ if (isItemSelected(item)) {
114
+ selectedItems.value = selectedItems.value.filter(
115
+ (it) => !_isEqual(item, it),
116
+ );
117
+ areAllCurrentItemsSelected.value = false;
118
+ } else {
119
+ selectedItems.value.push(item);
120
+ }
152
121
  }
153
122
  };
154
123
 
@@ -165,26 +134,27 @@ const onToggleSelectAllCurrentItems = () => {
165
134
  }
166
135
 
167
136
  // If we have a selection spanning across more than the current page
168
- if (selectedItems.value.length > props.items.length) {
137
+ if (selectedItems.value.length > props.totalSize) {
169
138
  selectedItems.value = [] as unknown[];
170
139
  } else {
171
- // If all the current page is selected
140
+ // If only some are selected
172
141
  if (!areAllCurrentItemsSelected.value) {
173
142
  props.items.forEach((item) => {
174
143
  if (!isItemSelected(item)) {
175
144
  selectedItems.value.push(item);
176
145
  }
146
+ if (item?.childElement && !isItemSelected(item?.childElement)) {
147
+ selectedItems.value.push(item.childElement);
148
+ }
177
149
  });
178
150
  } else {
179
- // If nothing selected
180
- selectedItems.value = selectedItems.value.filter(
181
- (selectedItem) =>
182
- !props.items.find((item) => _isEqual(item, selectedItem)),
183
- );
151
+ // If all the current page is selected
152
+ selectedItems.value = [];
184
153
  }
185
154
  }
186
155
  };
187
156
 
157
+ // Blob animation
188
158
  const blob = ref<HTMLDivElement | null>(null);
189
159
  const mainComponent = ref<HTMLDivElement | null>(null);
190
160
 
@@ -220,6 +190,7 @@ const onMouseMove = (e: MouseEvent) => {
220
190
  };
221
191
 
222
192
  window.addEventListener('mousemove', onMouseMove);
193
+
223
194
  watch(
224
195
  () => props.items.length,
225
196
  () => {
@@ -282,7 +253,9 @@ onMounted(() => {
282
253
  <span
283
254
  class="select-mode-all"
284
255
  @click="selectAll"
285
- v-if="totalSize < maxSelectedSize"
256
+ v-if="
257
+ totalSize < maxSelectedSize && selectMode !== SelectMode.SINGLE
258
+ "
286
259
  >
287
260
  Sélectionner la totalité des {{ totalSize }} éléments
288
261
  </span>
@@ -305,14 +278,11 @@ onMounted(() => {
305
278
  :class="tableClass"
306
279
  ref="mainComponent"
307
280
  >
308
- <thead
309
- ref="thead"
310
- class="bms-table__header"
311
- :class="{ stuck: isHeaderStuck }"
312
- >
281
+ <thead ref="thead" class="bms-table__header">
313
282
  <tr class="bms-table__headers bms-table__row">
314
283
  <th v-if="selectable">
315
284
  <UiBmsInputCheckbox
285
+ v-if="selectMode !== SelectMode.SINGLE"
316
286
  name="select-all"
317
287
  :disabled="items.length === 0 || selectableDisabled"
318
288
  @update:model-value="onToggleSelectAllCurrentItems"
@@ -324,7 +294,7 @@ onMounted(() => {
324
294
  :style="{
325
295
  '--table-cell-width': header?.width || undefined,
326
296
  }"
327
- :class="getHeaderClasses(header)"
297
+ :class="getHeaderClasses(header, sort)"
328
298
  :key="header.label"
329
299
  @click="emits('clickHeader', header)"
330
300
  >
@@ -360,8 +330,7 @@ onMounted(() => {
360
330
  <slot name="default" :row="row"></slot>
361
331
  </template>
362
332
  </UiBmsTableRow>
363
- <!-- FIXME typing -->
364
- <template v-if="(item as any)?.childElement">
333
+ <template v-if="item?.childElement">
365
334
  <slot name="child-element">
366
335
  <UiBmsTableRow
367
336
  is-child-element
@@ -371,6 +340,7 @@ onMounted(() => {
371
340
  :headers="filteredHeaders"
372
341
  :select-mode="selectMode"
373
342
  :selectable-disabled="selectableDisabled"
343
+ @select="onItemSelect"
374
344
  >
375
345
  <template
376
346
  v-for="cell in headers"
@@ -579,16 +549,6 @@ onMounted(() => {
579
549
  border-top-left-radius: var(--table-cell-radius);
580
550
  }
581
551
 
582
- .stuck {
583
- th:first-child {
584
- border-top-left-radius: 0;
585
- }
586
-
587
- th:last-child {
588
- border-top-right-radius: 0;
589
- }
590
- }
591
-
592
552
  th:last-child {
593
553
  border-top-right-radius: var(--table-cell-radius);
594
554
  }
@@ -2,7 +2,7 @@
2
2
  <tr
3
3
  class="bms-table__row"
4
4
  :class="{
5
- 'bms-table__row--selected': isItemSelected(item),
5
+ 'bms-table__row--selected': isItemSelected(currentItem),
6
6
  'bms-table__row--disabled': isChildElement,
7
7
  'bms-table__row--dense': dense,
8
8
  }"
@@ -13,11 +13,20 @@
13
13
  tooltip-text="Vous ne pouvez pas désélectionner un élément unitairement si vous avez choisi de sélectionner la totalité des éléments"
14
14
  :activated="selectMode === SelectMode.ALL"
15
15
  >
16
+ <BmsInputRadio
17
+ v-if="selectMode === SelectMode.SINGLE"
18
+ :name="uuid()"
19
+ :disabled="selectableDisabled"
20
+ :value="currentItem"
21
+ :model-value="isItemSelected(currentItem) ? currentItem : null"
22
+ @update:model-value="emits('select', currentItem)"
23
+ />
16
24
  <UiBmsInputCheckbox
25
+ v-else
17
26
  :name="uuid()"
18
27
  :disabled="selectMode === SelectMode.ALL || selectableDisabled"
19
- :model-value="isItemSelected(item)"
20
- @update:model-value="emits('select', item)"
28
+ :model-value="isItemSelected(currentItem)"
29
+ @update:model-value="emits('select', currentItem)"
21
30
  />
22
31
  </BmsTooltip>
23
32
  </td>
@@ -69,6 +78,8 @@ import UiBmsInputCheckbox from '../form/UiBmsInputCheckbox.vue';
69
78
  import BmsTooltip from '../feedback/BmsTooltip.vue';
70
79
  import { CornerDownRight } from 'lucide-vue-next';
71
80
  import UiBmsTableCell from './UiBmsTableCell.vue';
81
+ import BmsInputRadio from '../form/BmsInputRadio.vue';
82
+ import { computed } from 'vue';
72
83
 
73
84
  interface Props {
74
85
  item: any;
@@ -87,6 +98,10 @@ const props = withDefaults(defineProps<Props>(), {
87
98
 
88
99
  const emits = defineEmits<{ select: [item: any] }>();
89
100
 
101
+ const currentItem = computed(() =>
102
+ props.isChildElement ? props.item.childElement : props.item,
103
+ );
104
+
90
105
  const isItemSelected = (item: unknown): boolean => {
91
106
  return (
92
107
  props.selectMode === SelectMode.ALL ||
@@ -1,10 +1,4 @@
1
- import {
2
- Canvas,
3
- Meta,
4
- Story,
5
- Stories,
6
- Controls,
7
- } from '@storybook/addon-docs/blocks';
1
+ import { Meta } from '@storybook/addon-docs/blocks';
8
2
 
9
3
  ![](./CoverBmsUI.png)
10
4
 
@@ -21,3 +15,75 @@ bms UI focuses on the essentials, we concentrate on what is useful for interface
21
15
  ## Design System by constraint
22
16
 
23
17
  A constraint-based design system is designed to offer a consistent user experience across all products that use it. By limiting possible interpretations, this type of design system provides a clear and consistent logic that facilitates understanding and use of the products. Constraints can be applied to visual elements such as colors, typography, and font sizes, as well as interactive elements such as buttons and menus. By using a constraint-based design system, users can interact with products with confidence, knowing that the design elements will be consistent and predictable.
18
+
19
+ ## 📖 Documentation of the **bms UI** Design System
20
+
21
+ > **Mission** – Provide a **single Design System** dedicated to the Sipa group’s Back‑Office tools, limiting the use of the Vue framework and offering a constraint‑driven approach to interface design and construction. Goal: reduce costs, ensure visual and functional consistency, and simplify maintenance.
22
+
23
+ ---
24
+
25
+ ### 1️⃣ Context & Objectives
26
+
27
+ - **Who?**
28
+ The bms UI Design System is actually maintained by the frontend team of **bms**.
29
+
30
+ - **Why?**
31
+ - Uniform the look‑and‑feel of internal products and applications.
32
+ - Limit Vue code sprawl and avoid ad‑hoc solutions.
33
+ - Speed up development with reusable, tested components.
34
+
35
+ - **Target audience**
36
+ Front‑end developers, designers, QA engineers, project managers, and anyone involved in creating or maintaining Back‑Office interfaces.
37
+
38
+ ### 2️⃣ Core Principles
39
+
40
+ #### 2.1 Focused – Essentials First
41
+
42
+ > _“We concentrate on what truly matters for interfaces, nothing superfluous.”_
43
+
44
+ - **Functional minimalism** – Each component must solve a clearly identified need.
45
+ - **Dependency pruning** – Keep external imports to a minimum to reduce bundle size.
46
+ - **Concise documentation** – Every component includes essential props, available slots, and a usage example.
47
+
48
+ #### 2.2 Design System by Constraint – Constraint as a Cohesion Lever
49
+
50
+ > _“By limiting possible interpretations, we create a clear and predictable logic.”_
51
+
52
+ | Domain | Applied Constraints |
53
+ | ---------------- | ---------------------------------------------------------- |
54
+ | **Colors** | Restricted palette – no free‑form colors. |
55
+ | **Typography** | One font families, predefined sizes. |
56
+ | **Spacing** | Margin/padding scale based on a 8 px step. |
57
+ | **Interactions** | Standardised states (default, hover, focus, disabled). |
58
+ | **Behavior** | Uniform Vue API (`v-model` where applicable, named slots). |
59
+
60
+ These constraints are **declarative** – they’re baked directly into components (e.g., `mode="danger"` to have a red button), preventing stylistic drift.
61
+
62
+ ### 3️⃣ Project Architecture
63
+
64
+ - **Single export** – `import BmsButton from '../components/button/BmsButton.vue';`
65
+ - **Integrated Storybook** – Each component has a `*.stories.js` file showcasing the UI and the props & slots for each component.```
66
+ - **Typescript components** - Each component as typed props & events, and the relevant types are exported by the library (e.g. TableHeader, Caption, etc.)
67
+
68
+ ### 4️⃣ Usage Guide
69
+
70
+ #### 4.1 Installation
71
+
72
+ Gitlab documentation https://gitlab.ouest-france.fr/sipa-ouest-france/platform/platform-library-vuejs-bms#installation
73
+
74
+ ### 5️⃣ Quick FAQ
75
+
76
+ - **Can I add a custom color?**
77
+ **Answer:** No – all colors must come from the defined palette. If a legitimate need arises, open a **Jira** ticket.
78
+
79
+ - **How do I handle a new breakpoint?**
80
+ **Answer:** There are no breakpoints – bmsUI only provides desktop‑only interfaces.
81
+
82
+ - **Does the design system support dark mode?**
83
+ **Answer:** No – theming is not available at this time.
84
+
85
+ ### 6️⃣ Contact & Support
86
+
87
+ - **Teams**: [#🎨bmsUI](https://teams.microsoft.com/l/channel/19%3Au9dPXr-JjoRoaLat-EyS-QEKit1ZzUwQ7G0VrzvDkTE1%40thread.tacv2/%F0%9F%8E%A8bmsUI?groupId=677fd2f9-de86-4bf9-a00c-71817f033ad3&tenantId=a59e9cc9-4ed4-43c4-9f1e-ca78d44b0072&ngc=true&allowXTenantAccess=)
88
+ - **Issues**: [#Jira] (⚠️ WIP)
89
+ - **Gitlab**: [Gitlab](https://gitlab.ouest-france.fr/sipa-ouest-france/platform/platform-library-vuejs-bms)
@@ -86,3 +86,22 @@ export const enforceActionsColumnHeader = (
86
86
  .filter((h) => !h?.action)
87
87
  .concat(actionsHeaders.length ? actionsHeaders[0] : []);
88
88
  };
89
+
90
+ const getAlignClass = (header: TableHeader) => {
91
+ const align = !header.align ? 'start' : header.align;
92
+ return `u-text-align-${align}`;
93
+ };
94
+
95
+ export const getHeaderClasses = (header: TableHeader, sort: Sort): string[] => {
96
+ const classes = [getAlignClass(header), 'bms-table__header-cell'];
97
+ if (header.class) {
98
+ classes.push(header.class);
99
+ }
100
+ if (header.sortable) {
101
+ classes.push('sortable');
102
+ }
103
+ if (sort.key === header.key) {
104
+ classes.push('sorted');
105
+ }
106
+ return classes;
107
+ };
@@ -68,4 +68,5 @@ export interface SavedFilter {
68
68
  export enum SelectMode {
69
69
  ALL = 'all',
70
70
  DEFAULT = 'default',
71
+ SINGLE = 'single',
71
72
  }
@@ -30,10 +30,7 @@
30
30
  <div>Nb Selected items: {{ selectedItems.length }}</div>
31
31
  <div>
32
32
  Selected items:
33
- {{
34
- selectMode === SelectMode.ALL &&
35
- selectedItems?.map((i: any) => '#' + i.id + ' : ' + i.name).join(', ')
36
- }}
33
+ {{ selectedItems?.map((i: any) => '#' + i.id + ' : ' + i.name).join(', ') }}
37
34
  </div>
38
35
  </template>
39
36