better-table 1.0.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/LICENSE +201 -0
- package/README.md +5 -0
- package/dist/better-table.cjs.js +2 -0
- package/dist/better-table.cjs.js.map +1 -0
- package/dist/better-table.css +1 -0
- package/dist/better-table.es.js +1182 -0
- package/dist/better-table.es.js.map +1 -0
- package/dist/components/BetterTable/components/Table.d.ts +4 -0
- package/dist/components/BetterTable/components/TableActions.d.ts +8 -0
- package/dist/components/BetterTable/components/TableBody.d.ts +4 -0
- package/dist/components/BetterTable/components/TableCell.d.ts +9 -0
- package/dist/components/BetterTable/components/TableEmpty.d.ts +1 -0
- package/dist/components/BetterTable/components/TableHeader.d.ts +4 -0
- package/dist/components/BetterTable/components/TableHeaderCell.d.ts +7 -0
- package/dist/components/BetterTable/components/TableLoading.d.ts +6 -0
- package/dist/components/BetterTable/components/TableModal.d.ts +1 -0
- package/dist/components/BetterTable/components/TablePagination.d.ts +1 -0
- package/dist/components/BetterTable/components/TableRow.d.ts +8 -0
- package/dist/components/BetterTable/components/TableToolbar.d.ts +4 -0
- package/dist/components/BetterTable/components/index.d.ts +12 -0
- package/dist/components/BetterTable/context/TableContext.d.ts +68 -0
- package/dist/components/BetterTable/context/index.d.ts +2 -0
- package/dist/components/BetterTable/hooks/index.d.ts +5 -0
- package/dist/components/BetterTable/hooks/useTableFilter.d.ts +17 -0
- package/dist/components/BetterTable/hooks/useTablePagination.d.ts +23 -0
- package/dist/components/BetterTable/hooks/useTableSearch.d.ts +18 -0
- package/dist/components/BetterTable/hooks/useTableSelection.d.ts +23 -0
- package/dist/components/BetterTable/hooks/useTableSort.d.ts +15 -0
- package/dist/components/BetterTable/index.d.ts +13 -0
- package/dist/components/BetterTable/types.d.ts +236 -0
- package/dist/components/BetterTable/utils/filterData.d.ts +18 -0
- package/dist/components/BetterTable/utils/getValueFromPath.d.ts +7 -0
- package/dist/components/BetterTable/utils/index.d.ts +3 -0
- package/dist/components/BetterTable/utils/sortData.d.ts +9 -0
- package/dist/index.d.ts +19 -0
- package/dist/styles.cjs +2 -0
- package/dist/styles.cjs.map +1 -0
- package/dist/styles.d.ts +0 -0
- package/dist/styles.js +2 -0
- package/dist/styles.js.map +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { ReactNode, CSSProperties } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Tipo base para los datos de la tabla
|
|
4
|
+
* Permite extensión con tipos específicos del usuario
|
|
5
|
+
*/
|
|
6
|
+
export type TableData = Record<string, unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Configuración de columna con tipado genérico
|
|
9
|
+
*/
|
|
10
|
+
export interface Column<T extends TableData = TableData> {
|
|
11
|
+
/** Identificador único de la columna */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Key para acceder al dato (soporta dot notation: 'user.profile.name') */
|
|
14
|
+
accessor: keyof T | string;
|
|
15
|
+
/** Texto visible en el header */
|
|
16
|
+
header: string;
|
|
17
|
+
/** Tipo de dato para filtrado y renderizado */
|
|
18
|
+
type?: "string" | "number" | "boolean" | "date" | "custom";
|
|
19
|
+
/** Render personalizado de celda */
|
|
20
|
+
cell?: (value: unknown, row: T, rowIndex: number) => ReactNode;
|
|
21
|
+
/** Render personalizado de header */
|
|
22
|
+
headerCell?: (column: Column<T>) => ReactNode;
|
|
23
|
+
/** ¿Columna ordenable? */
|
|
24
|
+
sortable?: boolean;
|
|
25
|
+
/** ¿Columna filtrable? */
|
|
26
|
+
filterable?: boolean;
|
|
27
|
+
/** Ancho de columna */
|
|
28
|
+
width?: string | number;
|
|
29
|
+
/** Alineación del contenido */
|
|
30
|
+
align?: "left" | "center" | "right";
|
|
31
|
+
/** Columna oculta */
|
|
32
|
+
hidden?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Acción de fila individual
|
|
36
|
+
*/
|
|
37
|
+
export interface RowAction<T extends TableData = TableData> {
|
|
38
|
+
/** Identificador único */
|
|
39
|
+
id: string;
|
|
40
|
+
/** Etiqueta de la acción */
|
|
41
|
+
label: string;
|
|
42
|
+
/** Icono (string, emoji, o componente) */
|
|
43
|
+
icon?: ReactNode;
|
|
44
|
+
/** Modo de ejecución */
|
|
45
|
+
mode: "callback" | "modal" | "link";
|
|
46
|
+
/** Callback cuando mode='callback' */
|
|
47
|
+
onClick?: (row: T, rowIndex: number) => void;
|
|
48
|
+
/** Componente para modal cuando mode='modal' */
|
|
49
|
+
modalContent?: React.ComponentType<{
|
|
50
|
+
data: T;
|
|
51
|
+
onClose: () => void;
|
|
52
|
+
}>;
|
|
53
|
+
/** URL cuando mode='link' */
|
|
54
|
+
href?: string | ((row: T) => string);
|
|
55
|
+
/** ¿Mostrar acción condicionalmente? */
|
|
56
|
+
visible?: (row: T) => boolean;
|
|
57
|
+
/** ¿Deshabilitar acción condicionalmente? */
|
|
58
|
+
disabled?: (row: T) => boolean;
|
|
59
|
+
/** Variante visual */
|
|
60
|
+
variant?: "default" | "primary" | "danger" | "ghost";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Acción global (toolbar)
|
|
64
|
+
*/
|
|
65
|
+
export interface GlobalAction<T extends TableData = TableData> {
|
|
66
|
+
/** Identificador único */
|
|
67
|
+
id: string;
|
|
68
|
+
/** Etiqueta del botón */
|
|
69
|
+
label: string;
|
|
70
|
+
/** Icono */
|
|
71
|
+
icon?: ReactNode;
|
|
72
|
+
/** Callback de ejecución */
|
|
73
|
+
onClick: (selectedRows: T[], allData: T[]) => void;
|
|
74
|
+
/** ¿Requiere selección de filas? */
|
|
75
|
+
requiresSelection?: boolean;
|
|
76
|
+
/** Variante visual */
|
|
77
|
+
variant?: "default" | "primary" | "danger";
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Configuración de paginación
|
|
81
|
+
*/
|
|
82
|
+
export interface PaginationConfig {
|
|
83
|
+
/** Página actual (controlado) */
|
|
84
|
+
page?: number;
|
|
85
|
+
/** Items por página */
|
|
86
|
+
pageSize?: number;
|
|
87
|
+
/** Opciones de tamaño de página */
|
|
88
|
+
pageSizeOptions?: number[];
|
|
89
|
+
/** Total de items (para paginación del servidor) */
|
|
90
|
+
totalItems?: number;
|
|
91
|
+
/** Mostrar selector de tamaño de página */
|
|
92
|
+
showSizeChanger?: boolean;
|
|
93
|
+
/** Mostrar salto a página */
|
|
94
|
+
showQuickJumper?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Estado de ordenamiento
|
|
98
|
+
*/
|
|
99
|
+
export interface SortState {
|
|
100
|
+
columnId: string | null;
|
|
101
|
+
direction: "asc" | "desc";
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Estado de filtros
|
|
105
|
+
*/
|
|
106
|
+
export interface FilterState {
|
|
107
|
+
[columnId: string]: string | number | boolean | null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Personalización de estilos
|
|
111
|
+
*/
|
|
112
|
+
export interface TableClassNames {
|
|
113
|
+
/** Clase CSS del contenedor */
|
|
114
|
+
container?: string;
|
|
115
|
+
/** Clase CSS de la tabla */
|
|
116
|
+
table?: string;
|
|
117
|
+
/** Clase CSS del header */
|
|
118
|
+
header?: string;
|
|
119
|
+
/** Clase CSS del body */
|
|
120
|
+
body?: string;
|
|
121
|
+
/** Clase CSS de filas */
|
|
122
|
+
row?: string;
|
|
123
|
+
/** Clase CSS de celdas */
|
|
124
|
+
cell?: string;
|
|
125
|
+
/** Clase CSS de la paginación */
|
|
126
|
+
pagination?: string;
|
|
127
|
+
/** Clase CSS del toolbar */
|
|
128
|
+
toolbar?: string;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Textos personalizables (i18n)
|
|
132
|
+
*/
|
|
133
|
+
export interface TableLocale {
|
|
134
|
+
search?: string;
|
|
135
|
+
searchPlaceholder?: string;
|
|
136
|
+
noData?: string;
|
|
137
|
+
loading?: string;
|
|
138
|
+
page?: string;
|
|
139
|
+
of?: string;
|
|
140
|
+
items?: string;
|
|
141
|
+
selected?: string;
|
|
142
|
+
rowsPerPage?: string;
|
|
143
|
+
actions?: string;
|
|
144
|
+
sortAsc?: string;
|
|
145
|
+
sortDesc?: string;
|
|
146
|
+
filterBy?: string;
|
|
147
|
+
clearFilters?: string;
|
|
148
|
+
selectAll?: string;
|
|
149
|
+
deselectAll?: string;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Locale por defecto
|
|
153
|
+
*/
|
|
154
|
+
export declare const defaultLocale: TableLocale;
|
|
155
|
+
/**
|
|
156
|
+
* Props principales del componente BetterTable
|
|
157
|
+
*/
|
|
158
|
+
export interface BetterTableProps<T extends TableData = TableData> {
|
|
159
|
+
/** Array de datos a mostrar */
|
|
160
|
+
data: T[];
|
|
161
|
+
/** Configuración de columnas */
|
|
162
|
+
columns: Column<T>[];
|
|
163
|
+
/** Key único para identificar filas (default: 'id') */
|
|
164
|
+
rowKey?: keyof T | ((row: T, index: number) => string);
|
|
165
|
+
/** Acciones por fila */
|
|
166
|
+
rowActions?: RowAction<T>[];
|
|
167
|
+
/** Acciones globales (toolbar) */
|
|
168
|
+
globalActions?: GlobalAction<T>[];
|
|
169
|
+
/** Configuración de paginación (false para desactivar) */
|
|
170
|
+
pagination?: PaginationConfig | false;
|
|
171
|
+
/** Callback de cambio de página */
|
|
172
|
+
onPageChange?: (page: number, pageSize: number) => void;
|
|
173
|
+
/** Estado de ordenamiento (controlado) */
|
|
174
|
+
sort?: SortState;
|
|
175
|
+
/** Callback de cambio de ordenamiento */
|
|
176
|
+
onSortChange?: (sort: SortState) => void;
|
|
177
|
+
/** Estado de filtros (controlado) */
|
|
178
|
+
filters?: FilterState;
|
|
179
|
+
/** Callback de cambio de filtros */
|
|
180
|
+
onFilterChange?: (filters: FilterState) => void;
|
|
181
|
+
/** Mostrar barra de búsqueda */
|
|
182
|
+
searchable?: boolean;
|
|
183
|
+
/** Valor de búsqueda (controlado) */
|
|
184
|
+
searchValue?: string;
|
|
185
|
+
/** Callback de cambio de búsqueda */
|
|
186
|
+
onSearchChange?: (value: string) => void;
|
|
187
|
+
/** Columnas en las que buscar (default: todas) */
|
|
188
|
+
searchColumns?: string[];
|
|
189
|
+
/** Habilitar selección de filas */
|
|
190
|
+
selectable?: boolean;
|
|
191
|
+
/** Filas seleccionadas (controlado) */
|
|
192
|
+
selectedRows?: T[];
|
|
193
|
+
/** Callback de cambio de selección */
|
|
194
|
+
onSelectionChange?: (selectedRows: T[]) => void;
|
|
195
|
+
/** Modo de selección */
|
|
196
|
+
selectionMode?: "single" | "multiple";
|
|
197
|
+
/** Estado de carga */
|
|
198
|
+
loading?: boolean;
|
|
199
|
+
/** Componente de loading personalizado */
|
|
200
|
+
loadingComponent?: ReactNode;
|
|
201
|
+
/** Componente de estado vacío personalizado */
|
|
202
|
+
emptyComponent?: ReactNode;
|
|
203
|
+
/** Clases CSS personalizadas */
|
|
204
|
+
classNames?: TableClassNames;
|
|
205
|
+
/** Estilos inline personalizados */
|
|
206
|
+
styles?: {
|
|
207
|
+
container?: CSSProperties;
|
|
208
|
+
table?: CSSProperties;
|
|
209
|
+
header?: CSSProperties;
|
|
210
|
+
body?: CSSProperties;
|
|
211
|
+
row?: CSSProperties;
|
|
212
|
+
cell?: CSSProperties;
|
|
213
|
+
};
|
|
214
|
+
/** Textos personalizados */
|
|
215
|
+
locale?: TableLocale;
|
|
216
|
+
/** Header fijo al hacer scroll */
|
|
217
|
+
stickyHeader?: boolean;
|
|
218
|
+
/** Altura máxima (activa scroll interno) */
|
|
219
|
+
maxHeight?: string | number;
|
|
220
|
+
/** Mostrar bordes */
|
|
221
|
+
bordered?: boolean;
|
|
222
|
+
/** Filas con rayas alternas */
|
|
223
|
+
striped?: boolean;
|
|
224
|
+
/** Hover en filas */
|
|
225
|
+
hoverable?: boolean;
|
|
226
|
+
/** Tamaño de la tabla */
|
|
227
|
+
size?: "small" | "medium" | "large";
|
|
228
|
+
/** Callback al hacer click en una fila */
|
|
229
|
+
onRowClick?: (row: T, rowIndex: number) => void;
|
|
230
|
+
/** Callback al hacer doble click en una fila */
|
|
231
|
+
onRowDoubleClick?: (row: T, rowIndex: number) => void;
|
|
232
|
+
/** Descripción de la tabla para screen readers */
|
|
233
|
+
ariaLabel?: string;
|
|
234
|
+
/** ID del elemento que describe la tabla */
|
|
235
|
+
ariaDescribedBy?: string;
|
|
236
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TableData, Column, FilterState } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Filtra un array de datos basado en múltiples filtros de columna
|
|
4
|
+
* @param data - Array de datos a filtrar
|
|
5
|
+
* @param filters - Estado de filtros por columna
|
|
6
|
+
* @param columns - Configuración de columnas
|
|
7
|
+
* @returns Array filtrado (nueva referencia)
|
|
8
|
+
*/
|
|
9
|
+
export declare function filterData<T extends TableData>(data: T[], filters: FilterState, columns: Column<T>[]): T[];
|
|
10
|
+
/**
|
|
11
|
+
* Realiza una búsqueda global en los datos
|
|
12
|
+
* @param data - Array de datos
|
|
13
|
+
* @param searchValue - Valor a buscar
|
|
14
|
+
* @param columns - Columnas en las que buscar
|
|
15
|
+
* @param searchColumnIds - IDs de columnas específicas (opcional)
|
|
16
|
+
* @returns Array filtrado
|
|
17
|
+
*/
|
|
18
|
+
export declare function searchData<T extends TableData>(data: T[], searchValue: string, columns: Column<T>[], searchColumnIds?: string[]): T[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accede a propiedades anidadas de un objeto usando dot notation
|
|
3
|
+
* @param obj - Objeto del cual extraer el valor
|
|
4
|
+
* @param path - Ruta al valor (e.g., 'user.profile.name')
|
|
5
|
+
* @returns El valor encontrado o undefined
|
|
6
|
+
*/
|
|
7
|
+
export declare function getValueFromPath<T = unknown>(obj: Record<string, unknown>, path: string): T | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TableData } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Ordena un array de datos por una columna específica
|
|
4
|
+
* @param data - Array de datos a ordenar
|
|
5
|
+
* @param columnId - ID/accessor de la columna
|
|
6
|
+
* @param direction - Dirección del ordenamiento
|
|
7
|
+
* @returns Array ordenado (nueva referencia)
|
|
8
|
+
*/
|
|
9
|
+
export declare function sortData<T extends TableData>(data: T[], columnId: string, direction: "asc" | "desc"): T[];
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @juanb/better-table
|
|
3
|
+
* A modern, flexible, and fully typed React data table component
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
export { BetterTable } from './components/BetterTable';
|
|
8
|
+
export { defaultLocale } from './components/BetterTable/types';
|
|
9
|
+
export type { BetterTableProps, Column, RowAction, GlobalAction, PaginationConfig, SortState, FilterState, TableClassNames, TableLocale, TableData, } from './components/BetterTable/types';
|
|
10
|
+
export { useTableSort } from './components/BetterTable/hooks/useTableSort';
|
|
11
|
+
export { useTableFilter } from './components/BetterTable/hooks/useTableFilter';
|
|
12
|
+
export { useTablePagination } from './components/BetterTable/hooks/useTablePagination';
|
|
13
|
+
export { useTableSelection } from './components/BetterTable/hooks/useTableSelection';
|
|
14
|
+
export { useTableSearch } from './components/BetterTable/hooks/useTableSearch';
|
|
15
|
+
export { useTableContext, TableProvider, } from './components/BetterTable/context';
|
|
16
|
+
export type { TableContextValue } from './components/BetterTable/context';
|
|
17
|
+
export { getValueFromPath } from './components/BetterTable/utils/getValueFromPath';
|
|
18
|
+
export { sortData } from './components/BetterTable/utils/sortData';
|
|
19
|
+
export { filterData, searchData, } from './components/BetterTable/utils/filterData';
|
package/dist/styles.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
package/dist/styles.d.ts
ADDED
|
File without changes
|
package/dist/styles.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "better-table",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A modern, flexible, and fully typed React data table component",
|
|
5
|
+
"author": "Juan Puca",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/better-table.cjs.js",
|
|
9
|
+
"module": "./dist/better-table.es.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/better-table.es.js"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/better-table.cjs.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"./styles.css": "./dist/styles.css"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": [
|
|
30
|
+
"**/*.css"
|
|
31
|
+
],
|
|
32
|
+
"keywords": [
|
|
33
|
+
"react",
|
|
34
|
+
"table",
|
|
35
|
+
"data-table",
|
|
36
|
+
"datagrid",
|
|
37
|
+
"typescript",
|
|
38
|
+
"sorting",
|
|
39
|
+
"filtering",
|
|
40
|
+
"pagination",
|
|
41
|
+
"selection"
|
|
42
|
+
],
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/jrodrigopuca/BetterTable.git"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/jrodrigopuca/BetterTable/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/jrodrigopuca/BetterTable#readme",
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": ">=18.0.0",
|
|
53
|
+
"react-dom": ">=18.0.0"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"clsx": "^2.1.1"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
60
|
+
"@testing-library/react": "^16.1.0",
|
|
61
|
+
"@testing-library/user-event": "^14.5.2",
|
|
62
|
+
"@types/node": "^22.10.5",
|
|
63
|
+
"@types/react": "^19.0.6",
|
|
64
|
+
"@types/react-dom": "^19.0.2",
|
|
65
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
66
|
+
"@vitest/coverage-v8": "^4.0.0",
|
|
67
|
+
"@vitest/ui": "^4.0.18",
|
|
68
|
+
"jsdom": "^27.0.1",
|
|
69
|
+
"react": "^19.2.4",
|
|
70
|
+
"react-dom": "^19.2.4",
|
|
71
|
+
"typescript": "^5.7.3",
|
|
72
|
+
"vite": "^6.0.5",
|
|
73
|
+
"vite-plugin-dts": "^4.5.0",
|
|
74
|
+
"vitest": "^4.0.18"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"dev": "vite --config demo/vite.config.ts",
|
|
78
|
+
"build": "vite build",
|
|
79
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
80
|
+
"preview": "vite preview",
|
|
81
|
+
"test": "vitest --config vitest.config.ts",
|
|
82
|
+
"test:run": "vitest run --config vitest.config.ts",
|
|
83
|
+
"test:coverage": "vitest run --coverage --config vitest.config.ts",
|
|
84
|
+
"lint": "tsc --noEmit",
|
|
85
|
+
"prepublishOnly": "npm run build"
|
|
86
|
+
},
|
|
87
|
+
"browserslist": {
|
|
88
|
+
"production": [
|
|
89
|
+
">0.2%",
|
|
90
|
+
"not dead",
|
|
91
|
+
"not op_mini all"
|
|
92
|
+
],
|
|
93
|
+
"development": [
|
|
94
|
+
"last 1 chrome version",
|
|
95
|
+
"last 1 firefox version",
|
|
96
|
+
"last 1 safari version"
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
}
|