kelt-ui-kit-react 2.0.0 → 2.0.2

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,7 +1,7 @@
1
1
  {
2
2
  "name": "kelt-ui-kit-react",
3
3
  "type": "module",
4
- "version": "2.0.0",
4
+ "version": "2.0.2",
5
5
  "private": false,
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
package/src/App.menu.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import { NavLink } from "react-router-dom";
2
2
  import { ActionView } from "./action/Action.view";
3
+ import { AddressSearchView } from "./addressSearch/AddressSearch.view";
3
4
  import { BadgeView } from "./badge/Badge.view";
4
5
  import { ButtonView } from "./button/Button.view";
5
6
  import { ButtonFileView } from "./button/buttonFile/ButtonFile.view";
@@ -242,6 +243,13 @@ export const MenuList: MenuProps[] = [
242
243
  children: <NavLink to="/button-file">Button File</NavLink>,
243
244
  element: <ButtonFileView />,
244
245
  },
246
+ {
247
+ id: 30,
248
+ name: "Address search",
249
+ link: "/address-search",
250
+ children: <NavLink to="/address-search">Address search</NavLink>,
251
+ element: <AddressSearchView />,
252
+ },
245
253
  ];
246
254
 
247
255
  export const AppMenu = (): JSX.Element => {
@@ -0,0 +1,246 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import { Icon } from "../icon/Icon";
9
+ import {
10
+ GeocodedAddress,
11
+ GeocodingProvider,
12
+ } from "../geocoding/geocoding.interface";
13
+ import { geocodingService } from "../geocoding/GeocodingService";
14
+ import "./addressSearch.css";
15
+
16
+ export interface AddressSearchProps {
17
+ id?: string;
18
+ className?: string;
19
+ label?: string;
20
+ placeholder?: string;
21
+ value?: string;
22
+ disabled?: boolean;
23
+ minimumQueryLength?: number;
24
+ debounceTime?: number;
25
+ limit?: number;
26
+ provider?: GeocodingProvider;
27
+ allowManualEntry?: boolean;
28
+ manualEntryLabel?: string;
29
+ onChange?: (value: string) => void;
30
+ onManualEntry?: (addressLine: string) => void;
31
+ onSelect: (address: GeocodedAddress) => void;
32
+ }
33
+
34
+ export const AddressSearch = ({
35
+ id,
36
+ className,
37
+ label = "Rechercher une adresse",
38
+ placeholder = "Numéro, rue, code postal, ville",
39
+ value,
40
+ disabled = false,
41
+ minimumQueryLength = 3,
42
+ debounceTime = 300,
43
+ limit = 5,
44
+ provider = geocodingService,
45
+ allowManualEntry = true,
46
+ manualEntryLabel = "Je ne trouve pas mon adresse",
47
+ onChange,
48
+ onManualEntry,
49
+ onSelect,
50
+ }: AddressSearchProps) => {
51
+ const generatedId = useId();
52
+ const inputId = id ?? `address-search-${generatedId}`;
53
+ const listboxId = `${inputId}-results`;
54
+ const [query, setQuery] = useState(value ?? "");
55
+ const [results, setResults] = useState<GeocodedAddress[]>([]);
56
+ const [loading, setLoading] = useState(false);
57
+ const [error, setError] = useState(false);
58
+ const [isOpen, setIsOpen] = useState(false);
59
+ const [activeIndex, setActiveIndex] = useState(-1);
60
+ const containerRef = useRef<HTMLDivElement>(null);
61
+ const requestRef = useRef<AbortController>();
62
+ const skipNextSearchRef = useRef(false);
63
+
64
+ useEffect(() => {
65
+ if (value !== undefined) setQuery(value);
66
+ }, [value]);
67
+
68
+ useEffect(() => {
69
+ const handlePointerDown = (event: PointerEvent) => {
70
+ if (!containerRef.current?.contains(event.target as Node)) {
71
+ setIsOpen(false);
72
+ }
73
+ };
74
+
75
+ document.addEventListener("pointerdown", handlePointerDown);
76
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
77
+ }, []);
78
+
79
+ useEffect(() => {
80
+ if (skipNextSearchRef.current) {
81
+ skipNextSearchRef.current = false;
82
+ return;
83
+ }
84
+
85
+ const normalizedQuery = query.trim();
86
+ if (normalizedQuery.length < minimumQueryLength || disabled) {
87
+ requestRef.current?.abort();
88
+ setResults([]);
89
+ setLoading(false);
90
+ setError(false);
91
+ setIsOpen(false);
92
+ return;
93
+ }
94
+
95
+ const timeout = window.setTimeout(async () => {
96
+ requestRef.current?.abort();
97
+ const controller = new AbortController();
98
+ requestRef.current = controller;
99
+ setLoading(true);
100
+ setError(false);
101
+
102
+ try {
103
+ const addresses = await provider.search(normalizedQuery, {
104
+ limit,
105
+ signal: controller.signal,
106
+ });
107
+ setResults(addresses);
108
+ setActiveIndex(-1);
109
+ setIsOpen(true);
110
+ } catch (requestError) {
111
+ if ((requestError as Error).name !== "AbortError") {
112
+ setResults([]);
113
+ setError(true);
114
+ setIsOpen(true);
115
+ }
116
+ } finally {
117
+ if (!controller.signal.aborted) setLoading(false);
118
+ }
119
+ }, debounceTime);
120
+
121
+ return () => {
122
+ window.clearTimeout(timeout);
123
+ requestRef.current?.abort();
124
+ };
125
+ }, [debounceTime, disabled, limit, minimumQueryLength, provider, query]);
126
+
127
+ const selectAddress = useCallback(
128
+ (address: GeocodedAddress) => {
129
+ requestRef.current?.abort();
130
+ skipNextSearchRef.current = true;
131
+ setQuery(address.addressLine);
132
+ setResults([]);
133
+ setIsOpen(false);
134
+ setActiveIndex(-1);
135
+ onChange?.(address.addressLine);
136
+ onSelect(address);
137
+ },
138
+ [onChange, onSelect],
139
+ );
140
+
141
+ const selectManualAddress = useCallback(() => {
142
+ const addressLine = query.trim();
143
+ if (!addressLine) return;
144
+
145
+ requestRef.current?.abort();
146
+ skipNextSearchRef.current = true;
147
+ setQuery(addressLine);
148
+ setResults([]);
149
+ setIsOpen(false);
150
+ setActiveIndex(-1);
151
+ onChange?.(addressLine);
152
+ onManualEntry?.(addressLine);
153
+ }, [onChange, onManualEntry, query]);
154
+
155
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
156
+ if (!isOpen || results.length === 0) return;
157
+
158
+ if (event.key === "ArrowDown") {
159
+ event.preventDefault();
160
+ setActiveIndex((index) => (index + 1) % results.length);
161
+ } else if (event.key === "ArrowUp") {
162
+ event.preventDefault();
163
+ setActiveIndex((index) => (index <= 0 ? results.length - 1 : index - 1));
164
+ } else if (event.key === "Enter" && activeIndex >= 0) {
165
+ event.preventDefault();
166
+ selectAddress(results[activeIndex]);
167
+ } else if (event.key === "Escape") {
168
+ setIsOpen(false);
169
+ }
170
+ };
171
+
172
+ return (
173
+ <div ref={containerRef} className={`address-search ${className ?? ""}`}>
174
+ {label && <label htmlFor={inputId}>{label}</label>}
175
+ <div className="address-search-input">
176
+ <Icon classIcon="bi-geo-alt" />
177
+ <input
178
+ id={inputId}
179
+ type="search"
180
+ role="combobox"
181
+ aria-autocomplete="list"
182
+ aria-expanded={isOpen}
183
+ aria-controls={listboxId}
184
+ aria-activedescendant={
185
+ activeIndex >= 0 ? `${inputId}-option-${activeIndex}` : undefined
186
+ }
187
+ autoComplete="off"
188
+ disabled={disabled}
189
+ value={query}
190
+ placeholder={placeholder}
191
+ onChange={(event) => {
192
+ setQuery(event.target.value);
193
+ onChange?.(event.target.value);
194
+ }}
195
+ onFocus={() => results.length > 0 && setIsOpen(true)}
196
+ onKeyDown={handleKeyDown}
197
+ />
198
+ {loading && <span className="address-search-spinner" aria-hidden />}
199
+ </div>
200
+
201
+ {isOpen && (
202
+ <div className="address-search-results">
203
+ <div id={listboxId} role="listbox">
204
+ {error ? (
205
+ <div className="address-search-status" role="alert">
206
+ La recherche d'adresse est indisponible.
207
+ </div>
208
+ ) : results.length === 0 && !loading ? (
209
+ <div className="address-search-status">Aucune adresse trouvée.</div>
210
+ ) : (
211
+ results.map((address, index) => (
212
+ <button
213
+ id={`${inputId}-option-${index}`}
214
+ key={`${address.id}-${index}`}
215
+ type="button"
216
+ role="option"
217
+ aria-selected={activeIndex === index}
218
+ className={activeIndex === index ? "active" : ""}
219
+ onMouseDown={(event) => event.preventDefault()}
220
+ onMouseEnter={() => setActiveIndex(index)}
221
+ onClick={() => selectAddress(address)}
222
+ >
223
+ <span>{address.addressLine}</span>
224
+ <small>
225
+ {address.postalCode} {address.city}
226
+ </small>
227
+ </button>
228
+ ))
229
+ )}
230
+ </div>
231
+ {allowManualEntry && query.trim() && !loading && (
232
+ <button
233
+ type="button"
234
+ className="address-search-manual"
235
+ onMouseDown={(event) => event.preventDefault()}
236
+ onClick={selectManualAddress}
237
+ >
238
+ <Icon classIcon="bi-pencil" />
239
+ {manualEntryLabel}
240
+ </button>
241
+ )}
242
+ </div>
243
+ )}
244
+ </div>
245
+ );
246
+ };
@@ -0,0 +1,52 @@
1
+ import { useMemo } from "react";
2
+ import { DynamicForm } from "../form/Form";
3
+ import { TypeInputEnum } from "../form/form.enum";
4
+ import { FormInterface } from "../form/form.interface";
5
+
6
+ export const AddressSearchView = () => {
7
+ const fields = useMemo<FormInterface[]>(
8
+ () => [
9
+ {
10
+ name: "adresse1",
11
+ label: "Adresse",
12
+ type: TypeInputEnum.ADDRESS,
13
+ value: "",
14
+ col: { xs: 12, md: 6 },
15
+ addressSearch: {
16
+ mapAddress: (address) => ({
17
+ adresse1: address.addressLine,
18
+ codePostal: address.postalCode,
19
+ ville: address.city,
20
+ paysIso2: address.countryCode,
21
+ }),
22
+ },
23
+ },
24
+ {
25
+ name: "codePostal",
26
+ label: "Code postal",
27
+ type: TypeInputEnum.TEXT,
28
+ value: "",
29
+ col: { xs: 12, md: 6 },
30
+ },
31
+ {
32
+ name: "ville",
33
+ label: "Ville",
34
+ type: TypeInputEnum.TEXT,
35
+ value: "",
36
+ col: { xs: 12, md: 6 },
37
+ },
38
+ {
39
+ name: "paysIso2",
40
+ label: "Pays",
41
+ type: TypeInputEnum.TEXT,
42
+ value: "FR",
43
+ col: { xs: 12, md: 6 },
44
+ },
45
+ ],
46
+ [],
47
+ );
48
+
49
+ return (
50
+ <DynamicForm initialForm={fields} hideSubmit />
51
+ );
52
+ };
@@ -0,0 +1,133 @@
1
+ .address-search {
2
+ position: relative;
3
+ width: 100%;
4
+
5
+ >label {
6
+ display: block;
7
+ padding-bottom: 0.15rem;
8
+ font-size: 0.85rem;
9
+ font-weight: 600;
10
+ }
11
+ }
12
+
13
+ .address-search-input {
14
+ position: relative;
15
+ display: flex;
16
+ align-items: center;
17
+
18
+ >.bi {
19
+ position: absolute;
20
+ left: 0.75rem;
21
+ z-index: 1;
22
+ color: var(--primary-color);
23
+ }
24
+
25
+ >input {
26
+ box-sizing: border-box;
27
+ width: 100%;
28
+ min-width: 0;
29
+ padding-left: 2.25rem;
30
+ padding-right: 2.25rem;
31
+ }
32
+ }
33
+
34
+ .address-search-spinner {
35
+ position: absolute;
36
+ right: 0.75rem;
37
+ width: 0.9rem;
38
+ height: 0.9rem;
39
+ border: 2px solid #dedede;
40
+ border-top-color: var(--primary-color);
41
+ border-radius: 50%;
42
+ animation: address-search-spin 0.7s linear infinite;
43
+ }
44
+
45
+ .address-search-results {
46
+ position: absolute;
47
+ top: calc(100% + 0.25rem);
48
+ right: 0;
49
+ left: 0;
50
+ z-index: 20;
51
+ overflow: hidden;
52
+ border: 1px solid #cfbdbd;
53
+ border-radius: 4px;
54
+ background: #fff;
55
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.14);
56
+
57
+ [role="listbox"] {
58
+ max-height: 15rem;
59
+ overflow-y: auto;
60
+ }
61
+
62
+ [role="listbox"]>button {
63
+ display: flex;
64
+ width: 100%;
65
+ min-width: 0;
66
+ max-height: none;
67
+ margin: 0;
68
+ padding: 0.65rem 0.75rem;
69
+ flex-direction: column;
70
+ align-items: flex-start;
71
+ gap: 0.2rem;
72
+ border: 0;
73
+ border-bottom: 1px solid #ece8e8;
74
+ border-radius: 0;
75
+ background: #fff;
76
+ color: inherit;
77
+ font-size: 0.85rem;
78
+ line-height: 1.3;
79
+ text-align: left;
80
+ overflow-wrap: anywhere;
81
+ cursor: pointer;
82
+
83
+ &:last-child {
84
+ border-bottom: 0;
85
+ }
86
+
87
+ &:hover,
88
+ &.active {
89
+ background: color-mix(in srgb, var(--primary-color), white 92%);
90
+ }
91
+
92
+ >small {
93
+ color: #686868;
94
+ }
95
+ }
96
+ }
97
+
98
+ .address-search-manual {
99
+ display: flex;
100
+ width: 100%;
101
+ min-width: 0;
102
+ max-height: none;
103
+ margin: 0;
104
+ padding: 0.65rem 0.75rem;
105
+ align-items: center;
106
+ gap: 0.5rem;
107
+ border: 0;
108
+ border-top: 1px solid #dedede;
109
+ border-radius: 0;
110
+ background: #fff;
111
+ color: var(--primary-color);
112
+ font-size: 0.85rem;
113
+ font-weight: 600;
114
+ text-align: left;
115
+ cursor: pointer;
116
+
117
+ &:hover,
118
+ &:focus-visible {
119
+ background: color-mix(in srgb, var(--primary-color), white 92%);
120
+ }
121
+ }
122
+
123
+ .address-search-status {
124
+ padding: 0.75rem;
125
+ color: #686868;
126
+ font-size: 0.85rem;
127
+ }
128
+
129
+ @keyframes address-search-spin {
130
+ to {
131
+ transform: rotate(360deg);
132
+ }
133
+ }
@@ -1,5 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { createPortal } from "react-dom";
3
+ import { useIsMobile } from "../_core/hooks/useIsMobile";
4
+ import { Card } from "../card/Card";
3
5
  import { TypeInputEnum } from "../form/form.enum";
4
6
  import { Input } from "../form/input/Input";
5
7
  import { Icon } from "../icon/Icon";
@@ -42,6 +44,7 @@ export type DataTableProps<T> = {
42
44
  onRowClassName?: (row: T) => string | undefined;
43
45
  rowStyle?: (row: T) => React.CSSProperties | undefined;
44
46
  rowClassName?: (row: T) => string | undefined;
47
+ mobileCardView?: boolean;
45
48
  };
46
49
  export function DataTable<T>(props: DataTableProps<T>) {
47
50
  const {
@@ -70,8 +73,11 @@ export function DataTable<T>(props: DataTableProps<T>) {
70
73
  onRowClassName,
71
74
  rowStyle,
72
75
  rowClassName,
76
+ mobileCardView = false,
73
77
  } = props;
74
78
 
79
+ const isMobile = useIsMobile();
80
+ const showMobileCardView = mobileCardView && isMobile;
75
81
  const loaderRef = useRef<HTMLDivElement | null>(null);
76
82
  const footerActionRef = useRef<HTMLDivElement | null>(null);
77
83
  const hasAutoFocusedRef = useRef(false);
@@ -150,7 +156,9 @@ export function DataTable<T>(props: DataTableProps<T>) {
150
156
  return;
151
157
  }
152
158
 
153
- const table = document.getElementById(id);
159
+ const table =
160
+ document.getElementById(`${id}-mobile`) ??
161
+ document.getElementById(id);
154
162
  const eligibleColumnIndexes = columns.reduce<number[]>(
155
163
  (indexes, column, idx) => {
156
164
  if (
@@ -232,7 +240,8 @@ export function DataTable<T>(props: DataTableProps<T>) {
232
240
  return;
233
241
  }
234
242
 
235
- const table = document.getElementById(id);
243
+ const table =
244
+ document.getElementById(`${id}-mobile`) ?? document.getElementById(id);
236
245
  if (!table) {
237
246
  return;
238
247
  }
@@ -264,7 +273,7 @@ export function DataTable<T>(props: DataTableProps<T>) {
264
273
  for (let offset = 1; offset <= data.length; offset += 1) {
265
274
  const nextRowIndex = (rowIndex + offset) % data.length;
266
275
  const cell = table.querySelector(
267
- `tbody td[data-row-index="${nextRowIndex}"][data-column-index="${firstEligibleColumnIndex}"]`,
276
+ `[data-row-index="${nextRowIndex}"][data-column-index="${firstEligibleColumnIndex}"]`,
268
277
  );
269
278
 
270
279
  if (!(cell instanceof HTMLElement)) {
@@ -365,9 +374,10 @@ export function DataTable<T>(props: DataTableProps<T>) {
365
374
  return;
366
375
  }
367
376
 
368
- const table = document.getElementById(id);
377
+ const table =
378
+ document.getElementById(`${id}-mobile`) ?? document.getElementById(id);
369
379
  const firstCell = table?.querySelector(
370
- `tbody td[data-row-index="0"][data-column-index="${firstEditableColumnIndex}"]`,
380
+ `[data-row-index="0"][data-column-index="${firstEditableColumnIndex}"]`,
371
381
  );
372
382
 
373
383
  if (!(firstCell instanceof HTMLElement)) {
@@ -462,7 +472,105 @@ export function DataTable<T>(props: DataTableProps<T>) {
462
472
  >
463
473
  {name && <h2 className="mb-2">{name}</h2>}
464
474
  {element}
465
- <table className={`data-table ${className ? className : ""}`} id={id}>
475
+ {showMobileCardView && (
476
+ <div
477
+ id={`${id}-mobile`}
478
+ className={`data-table-mobile-cards ${className ? className : ""}`}
479
+ >
480
+ {loading && <Loader display={loading} />}
481
+ {!loading &&
482
+ data.map((row, rowIndex) => {
483
+ const customStyle = (onRowStyle ?? rowStyle)?.(row);
484
+ const customClass =
485
+ (onRowClassName ?? rowClassName)?.(row) ?? "";
486
+ const rowId = getRowId
487
+ ? getRowId(row)
488
+ : ((row as any).id ?? row);
489
+
490
+ return (
491
+ <Card
492
+ key={rowId as React.Key}
493
+ item={{ title: "" }}
494
+ className={`data-table-mobile-card ${customClass}`.trim()}
495
+ handleClick={onRowClick ? () => onRowClick(row) : undefined}
496
+ >
497
+ <div
498
+ className="data-table-mobile-card__content"
499
+ style={customStyle}
500
+ data-row-index={rowIndex}
501
+ onMouseDown={(event) => {
502
+ if (isInteractiveClick(event.target)) {
503
+ event.stopPropagation();
504
+ }
505
+ }}
506
+ >
507
+ {onSelectionChange && (
508
+ <div className="data-table-mobile-card__field data-table-mobile-card__selection">
509
+ <span className="data-table-mobile-card__label">
510
+ Sélection
511
+ </span>
512
+ <Input
513
+ id={`select-card-${rowIndex}`}
514
+ name={`select-card-${rowIndex}`}
515
+ type={TypeInputEnum.CHECKBOX}
516
+ disabled={
517
+ limitSelect !== undefined &&
518
+ selectedRowIds.size >= limitSelect &&
519
+ !selectedRowIds.has(rowId)
520
+ }
521
+ checked={selectedRowIds.has(rowId)}
522
+ onChange={(event) => toggleRow(event, row)}
523
+ />
524
+ </div>
525
+ )}
526
+ {columns.map((column, columnIndex) => (
527
+ <div
528
+ className="data-table-mobile-card__field"
529
+ key={column.id}
530
+ >
531
+ <span className="data-table-mobile-card__label">
532
+ {column.label}
533
+ </span>
534
+ <DataTableRow
535
+ container="card"
536
+ column={column}
537
+ idx={columnIndex}
538
+ row={row}
539
+ rowIndex={rowIndex}
540
+ onEnterNextColumn={focusNextColumn}
541
+ />
542
+ </div>
543
+ ))}
544
+ {actions.length > 0 && (
545
+ <div className="data-table-mobile-card__actions card-actions">
546
+ {actions.map((action, actionIndex) => (
547
+ <DataTableAction
548
+ key={action.id}
549
+ action={action}
550
+ row={row}
551
+ rowIndex={rowIndex}
552
+ index={actionIndex}
553
+ onFocusNextRow={focusNextRow}
554
+ />
555
+ ))}
556
+ </div>
557
+ )}
558
+ </div>
559
+ </Card>
560
+ );
561
+ })}
562
+ {!loading && data.length === 0 && (
563
+ <div className="data-table-mobile-cards__empty">
564
+ {emptyMessage ?? "Aucune donnée"}
565
+ </div>
566
+ )}
567
+ </div>
568
+ )}
569
+ <table
570
+ className={`data-table ${className ? className : ""}`}
571
+ id={id}
572
+ style={showMobileCardView ? { display: "none" } : undefined}
573
+ >
466
574
  <thead>
467
575
  <tr>
468
576
  {onSelectionChange && (
@@ -62,6 +62,7 @@ export const DataTableView = () => {
62
62
  columns={dataTableColumns}
63
63
  id="datatable-example"
64
64
  name={"DataTable"}
65
+ mobileCardView
65
66
  onSelectionChange={(selectedRows) => {
66
67
  console.log("Selected Rows: ", selectedRows);
67
68
  }}
@@ -47,6 +47,54 @@
47
47
  overflow: auto;
48
48
  position: relative;
49
49
 
50
+ .data-table-mobile-cards {
51
+ display: grid;
52
+ gap: 0.75rem;
53
+
54
+ .data-table-mobile-card {
55
+ width: 100%;
56
+ max-width: none;
57
+ height: auto;
58
+ padding: 0;
59
+ border-radius: 8px;
60
+ }
61
+
62
+ .data-table-mobile-card__content {
63
+ width: 100%;
64
+ min-width: 0;
65
+ }
66
+
67
+ .data-table-mobile-card__field {
68
+ display: grid;
69
+ grid-template-columns: minmax(90px, 35%) minmax(0, 1fr);
70
+ gap: 0.75rem;
71
+ align-items: center;
72
+ padding: 0.625rem 0.75rem;
73
+ border-bottom: 1px solid #e5e5e5;
74
+ }
75
+
76
+ .data-table-mobile-card__label {
77
+ color: var(--tertiary-color);
78
+ font-size: 0.875rem;
79
+ font-weight: 600;
80
+ overflow-wrap: anywhere;
81
+ }
82
+
83
+ .data-table-mobile-card__actions {
84
+ display: flex;
85
+ gap: 0.5rem;
86
+ align-items: center;
87
+ justify-content: flex-end;
88
+ padding: 0.75rem;
89
+ }
90
+
91
+ .data-table-mobile-cards__empty {
92
+ padding: 1rem;
93
+ text-align: center;
94
+ color: #666;
95
+ }
96
+ }
97
+
50
98
  &.data-table-container--with-footer-action {
51
99
  scroll-padding-bottom: 80px;
52
100
  }