proje-react-panel 1.0.15 → 1.0.17-test-8
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/.vscode/launch.json +10 -0
- package/dist/components/components/FormField.d.ts +6 -1
- package/dist/components/components/InnerForm.d.ts +9 -4
- package/dist/components/components/Uploader.d.ts +8 -0
- package/dist/components/components/index.d.ts +1 -1
- package/dist/components/components/list/Datagrid.d.ts +9 -0
- package/dist/components/components/list/EmptyList.d.ts +2 -0
- package/dist/components/components/list/FilterPopup.d.ts +11 -0
- package/dist/components/components/list/ListPage.d.ts +20 -0
- package/dist/components/components/list/Pagination.d.ts +11 -0
- package/dist/components/components/list/index.d.ts +0 -0
- package/dist/components/list/Datagrid.d.ts +8 -4
- package/dist/components/list/EmptyList.d.ts +2 -0
- package/dist/components/list/FilterPopup.d.ts +10 -0
- package/dist/components/pages/FormPage.d.ts +10 -3
- package/dist/components/pages/ListPage.d.ts +2 -1
- package/dist/decorators/form/Input.d.ts +8 -3
- package/dist/decorators/list/Cell.d.ts +14 -2
- package/dist/decorators/list/List.d.ts +26 -4
- package/dist/decorators/list/ListData.d.ts +4 -4
- package/dist/decorators/list/getListFields.d.ts +2 -2
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.esm.js +1 -1
- package/dist/types/AnyClass.d.ts +1 -1
- package/dist/types/ScreenCreatorData.d.ts +5 -5
- package/package.json +9 -3
- package/src/assets/icons/svg/create.svg +9 -0
- package/src/assets/icons/svg/filter.svg +3 -0
- package/src/assets/icons/svg/pencil.svg +8 -0
- package/src/assets/icons/svg/search.svg +8 -0
- package/src/assets/icons/svg/trash.svg +8 -0
- package/src/components/components/FormField.tsx +52 -9
- package/src/components/components/InnerForm.tsx +53 -15
- package/src/components/components/Uploader.tsx +66 -0
- package/src/components/components/index.ts +2 -2
- package/src/components/components/list/Datagrid.tsx +135 -0
- package/src/components/components/list/EmptyList.tsx +26 -0
- package/src/components/components/list/FilterPopup.tsx +202 -0
- package/src/components/components/list/ListPage.tsx +176 -0
- package/src/components/pages/FormPage.tsx +12 -3
- package/src/decorators/form/Input.ts +7 -4
- package/src/decorators/form/getFormFields.ts +1 -0
- package/src/decorators/list/Cell.ts +24 -14
- package/src/decorators/list/List.ts +26 -11
- package/src/decorators/list/ListData.ts +5 -5
- package/src/decorators/list/getListFields.ts +8 -8
- package/src/index.ts +8 -3
- package/src/styles/filter-popup.scss +134 -0
- package/src/styles/form.scss +2 -4
- package/src/styles/index.scss +18 -22
- package/src/styles/list.scss +151 -8
- package/src/styles/uploader.scss +86 -0
- package/src/types/AnyClass.ts +3 -1
- package/src/types/ScreenCreatorData.ts +12 -12
- package/src/types/svg.d.ts +5 -0
- package/src/components/components/ImageUploader.tsx +0 -301
- package/src/components/list/Datagrid.tsx +0 -101
- package/src/components/pages/ListPage.tsx +0 -85
- /package/src/components/{list → components/list}/Pagination.tsx +0 -0
- /package/src/components/{list → components/list}/index.ts +0 -0
- /package/src/styles/{_scrollbar.scss → utils/scrollbar.scss} +0 -0
@@ -0,0 +1,176 @@
|
|
1
|
+
import React, { useMemo, useCallback, useEffect, useState } from 'react';
|
2
|
+
import { Link, useParams, useNavigate } from 'react-router';
|
3
|
+
import { Datagrid } from './Datagrid';
|
4
|
+
import { ErrorComponent } from '../ErrorComponent';
|
5
|
+
import { LoadingScreen } from '../LoadingScreen';
|
6
|
+
import { AnyClass } from '../../../types/AnyClass';
|
7
|
+
import { getListFields } from '../../../decorators/list/getListFields';
|
8
|
+
import { Pagination } from './Pagination';
|
9
|
+
import { ListData } from '../../../decorators/list/ListData';
|
10
|
+
import CreateIcon from '../../../assets/icons/svg/create.svg';
|
11
|
+
import FilterIcon from '../../../assets/icons/svg/filter.svg';
|
12
|
+
import { FilterPopup } from './FilterPopup';
|
13
|
+
|
14
|
+
export interface GetDataParams {
|
15
|
+
page?: number;
|
16
|
+
limit?: number;
|
17
|
+
filters?: Record<string, any>;
|
18
|
+
}
|
19
|
+
|
20
|
+
export interface PaginatedResponse<T> {
|
21
|
+
data: T[];
|
22
|
+
total: number;
|
23
|
+
page: number;
|
24
|
+
limit: number;
|
25
|
+
}
|
26
|
+
|
27
|
+
export type GetDataForList<T> = (params: GetDataParams) => Promise<PaginatedResponse<T>>;
|
28
|
+
|
29
|
+
function ListHeader<T extends AnyClass>({
|
30
|
+
listData,
|
31
|
+
filtered,
|
32
|
+
onFilterClick,
|
33
|
+
customHeader,
|
34
|
+
}: {
|
35
|
+
listData: ListData<T>;
|
36
|
+
filtered: boolean;
|
37
|
+
onFilterClick: () => void;
|
38
|
+
customHeader?: React.ReactNode;
|
39
|
+
}) {
|
40
|
+
const fields = useMemo(() => listData.cells.filter(cell => !!cell.filter), [listData.cells]);
|
41
|
+
|
42
|
+
const header = listData.list?.headers;
|
43
|
+
return (
|
44
|
+
<div className="list-header">
|
45
|
+
<div className="header-title">{header?.title || 'List'}</div>
|
46
|
+
{customHeader && <div className="header-custom">{customHeader}</div>}
|
47
|
+
<div className="header-actions">
|
48
|
+
{!!fields.length && (
|
49
|
+
<button onClick={onFilterClick} className="filter-button">
|
50
|
+
<FilterIcon className={`icon icon-filter ${filtered ? 'active' : ''}`} />
|
51
|
+
Filter
|
52
|
+
</button>
|
53
|
+
)}
|
54
|
+
{header?.create && (
|
55
|
+
<Link to={header.create.path} className="create-button">
|
56
|
+
<CreateIcon className="icon icon-create" />
|
57
|
+
{header.create.label}
|
58
|
+
</Link>
|
59
|
+
)}
|
60
|
+
</div>
|
61
|
+
</div>
|
62
|
+
);
|
63
|
+
}
|
64
|
+
|
65
|
+
export function ListPage<T extends AnyClass>({
|
66
|
+
model,
|
67
|
+
getData,
|
68
|
+
onRemoveItem,
|
69
|
+
customHeader,
|
70
|
+
}: {
|
71
|
+
model: any;
|
72
|
+
getData: GetDataForList<T>;
|
73
|
+
customHeader?: React.ReactNode;
|
74
|
+
onRemoveItem?: (item: T) => Promise<void>;
|
75
|
+
}) {
|
76
|
+
const [loading, setLoading] = useState(true);
|
77
|
+
const [pagination, setPagination] = useState({ total: 0, page: 0, limit: 0 });
|
78
|
+
const [data, setData] = useState<any>(null);
|
79
|
+
const [error, setError] = useState<unknown>(null);
|
80
|
+
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
81
|
+
const [activeFilters, setActiveFilters] = useState<Record<string, string>>();
|
82
|
+
const listData = useMemo(() => getListFields(model), [model]);
|
83
|
+
const params = useParams();
|
84
|
+
const navigate = useNavigate();
|
85
|
+
|
86
|
+
const fetchData = useCallback(
|
87
|
+
async (page: number, filters?: Record<string, string>) => {
|
88
|
+
setLoading(true);
|
89
|
+
try {
|
90
|
+
const result = await getData({ page, filters: filters ?? activeFilters ?? {} });
|
91
|
+
setData(result.data);
|
92
|
+
setPagination({
|
93
|
+
total: result.total,
|
94
|
+
page: result.page,
|
95
|
+
limit: result.limit,
|
96
|
+
});
|
97
|
+
} catch (e) {
|
98
|
+
setError(e);
|
99
|
+
console.error(e);
|
100
|
+
} finally {
|
101
|
+
setLoading(false);
|
102
|
+
}
|
103
|
+
},
|
104
|
+
[getData, activeFilters]
|
105
|
+
);
|
106
|
+
|
107
|
+
useEffect(() => {
|
108
|
+
const searchParams = new URLSearchParams(location.search);
|
109
|
+
const filtersFromUrl: Record<string, string> = {};
|
110
|
+
searchParams.forEach((value, key) => {
|
111
|
+
filtersFromUrl[key] = value;
|
112
|
+
});
|
113
|
+
setActiveFilters(filtersFromUrl);
|
114
|
+
}, [location.search]);
|
115
|
+
|
116
|
+
useEffect(() => {
|
117
|
+
if (activeFilters) {
|
118
|
+
fetchData(parseInt(params.page as string) || 1, activeFilters);
|
119
|
+
}
|
120
|
+
}, [fetchData, params.page, activeFilters]);
|
121
|
+
|
122
|
+
const handleFilterApply = (filters: Record<string, any>) => {
|
123
|
+
setActiveFilters(filters);
|
124
|
+
|
125
|
+
// Convert filters to URLSearchParams
|
126
|
+
const searchParams = new URLSearchParams();
|
127
|
+
Object.entries(filters).forEach(([key, value]) => {
|
128
|
+
if (value !== undefined && value !== null && value !== '') {
|
129
|
+
searchParams.append(key, String(value));
|
130
|
+
}
|
131
|
+
});
|
132
|
+
const queryString = searchParams.toString();
|
133
|
+
const newUrl = `${location.pathname}${queryString ? `?${queryString}` : ''}`;
|
134
|
+
navigate(newUrl);
|
135
|
+
fetchData(1, filters); // Reset to first page when filters change
|
136
|
+
};
|
137
|
+
|
138
|
+
if (loading) return <LoadingScreen />;
|
139
|
+
if (error) return <ErrorComponent error={error} />;
|
140
|
+
|
141
|
+
return (
|
142
|
+
<div className="list">
|
143
|
+
<ListHeader
|
144
|
+
listData={listData}
|
145
|
+
filtered={!!(activeFilters && !!Object.keys(activeFilters).length)}
|
146
|
+
onFilterClick={() => setIsFilterOpen(true)}
|
147
|
+
customHeader={customHeader}
|
148
|
+
/>
|
149
|
+
<Datagrid
|
150
|
+
listData={listData}
|
151
|
+
data={data}
|
152
|
+
onRemoveItem={async (item: T) => {
|
153
|
+
if (onRemoveItem) {
|
154
|
+
if (
|
155
|
+
confirm('Are you sure you want to delete this item? This action cannot be undone.')
|
156
|
+
) {
|
157
|
+
await onRemoveItem(item);
|
158
|
+
//setData(data.filter((d: T) => d.id !== item.id));
|
159
|
+
await fetchData(pagination.page);
|
160
|
+
}
|
161
|
+
}
|
162
|
+
}}
|
163
|
+
/>
|
164
|
+
<div className="list-footer">
|
165
|
+
<Pagination pagination={pagination} onPageChange={fetchData} />
|
166
|
+
</div>
|
167
|
+
<FilterPopup
|
168
|
+
isOpen={isFilterOpen}
|
169
|
+
activeFilters={activeFilters}
|
170
|
+
onClose={() => setIsFilterOpen(false)}
|
171
|
+
onApplyFilters={handleFilterApply}
|
172
|
+
listData={listData}
|
173
|
+
/>
|
174
|
+
</div>
|
175
|
+
);
|
176
|
+
}
|
@@ -2,15 +2,19 @@ import React, { useMemo } from 'react';
|
|
2
2
|
import { InnerForm } from '../components';
|
3
3
|
import { AnyClass } from '../../types/AnyClass';
|
4
4
|
import { getFormFields } from '../../decorators/form/getFormFields';
|
5
|
+
import { InputOptions } from '../../decorators/form/Input';
|
5
6
|
|
6
|
-
export type GetDetailsDataFN<T> = (param: string) => Promise<T>;
|
7
|
+
export type GetDetailsDataFN<T> = (param: Record<string, string>) => Promise<T>;
|
7
8
|
export type OnSubmitFN<T> = (data: T) => Promise<T>;
|
8
9
|
|
9
10
|
export interface FormPageProps<T extends AnyClass> {
|
10
|
-
model: T
|
11
|
+
model: any; //TODO: use T not typeof T
|
11
12
|
getDetailsData?: GetDetailsDataFN<T>;
|
12
13
|
redirect?: string;
|
13
14
|
onSubmit: OnSubmitFN<T>;
|
15
|
+
onSelectPreloader?: (inputOptions: InputOptions) => Promise<{ label: string; value: string }[]>;
|
16
|
+
redirectBackOnSuccess?: boolean;
|
17
|
+
type?: 'json' | 'formData';
|
14
18
|
}
|
15
19
|
|
16
20
|
export function FormPage<T extends AnyClass>({
|
@@ -18,15 +22,20 @@ export function FormPage<T extends AnyClass>({
|
|
18
22
|
getDetailsData,
|
19
23
|
onSubmit,
|
20
24
|
redirect,
|
25
|
+
onSelectPreloader,
|
26
|
+
redirectBackOnSuccess = true,
|
27
|
+
type = 'json',
|
21
28
|
...rest
|
22
29
|
}: FormPageProps<T>) {
|
23
30
|
const formOptions = useMemo(() => getFormFields(model), [model]);
|
24
31
|
return (
|
25
32
|
<InnerForm
|
26
33
|
getDetailsData={getDetailsData}
|
27
|
-
redirect={redirect}
|
28
34
|
onSubmit={onSubmit}
|
29
35
|
formOptions={formOptions}
|
36
|
+
redirectBackOnSuccess={redirectBackOnSuccess}
|
37
|
+
onSelectPreloader={onSelectPreloader}
|
38
|
+
type={type}
|
30
39
|
/>
|
31
40
|
);
|
32
41
|
}
|
@@ -8,13 +8,15 @@ const isFieldSensitive = (fieldName: string): boolean => {
|
|
8
8
|
};
|
9
9
|
|
10
10
|
export interface InputOptions {
|
11
|
+
type?: 'input' | 'select' | 'textarea' | 'file-upload' | 'checkbox' | 'hidden' | 'nested';
|
12
|
+
inputType?: 'text' | 'email' | 'tel' | 'password' | 'number' | 'date';
|
11
13
|
name?: string;
|
12
14
|
label?: string;
|
13
15
|
placeholder?: string;
|
14
|
-
inputType?: 'text' | 'email' | 'tel' | 'password' | 'number' | 'date';
|
15
|
-
type?: 'input' | 'select' | 'textarea' | 'file-upload' | 'checkbox' | 'hidden';
|
16
|
-
selectOptions?: string[]; //TODO: label/value
|
17
16
|
cancelPasswordValidationOnEdit?: boolean;
|
17
|
+
options?: { value: string; label: string }[];
|
18
|
+
optionsPreload?: boolean;
|
19
|
+
nestedFields?: InputOptions[];
|
18
20
|
}
|
19
21
|
|
20
22
|
export function Input(options?: InputOptions): PropertyDecorator {
|
@@ -30,7 +32,8 @@ export function Input(options?: InputOptions): PropertyDecorator {
|
|
30
32
|
}
|
31
33
|
|
32
34
|
export function getInputFields<T extends AnyClass>(entityClass: T): InputOptions[] {
|
33
|
-
|
35
|
+
//TODO2: ANY IS NOT A GOOD SOLUTION, WE NEED TO FIND A BETTER WAY TO DO THIS
|
36
|
+
const prototype = (entityClass as any).prototype;
|
34
37
|
const inputFields: string[] = Reflect.getMetadata(INPUT_KEY, prototype) || [];
|
35
38
|
return inputFields.map(field => {
|
36
39
|
const fields = Reflect.getMetadata(`${INPUT_KEY.toString()}:${field}:options`, prototype) || {};
|
@@ -5,6 +5,7 @@ import { getFormConfiguration } from "./Form";
|
|
5
5
|
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
|
6
6
|
|
7
7
|
export function getFormFields<T extends AnyClass>(entityClass: T): FormOptions {
|
8
|
+
console.log(entityClass);
|
8
9
|
return {
|
9
10
|
resolver: classValidatorResolver(entityClass as any),
|
10
11
|
form: getFormConfiguration(entityClass),
|
@@ -1,22 +1,32 @@
|
|
1
|
-
import
|
1
|
+
import 'reflect-metadata';
|
2
2
|
|
3
|
-
export const CELL_KEY = Symbol(
|
3
|
+
export const CELL_KEY = Symbol('cell');
|
4
|
+
|
5
|
+
interface Filter {
|
6
|
+
type: 'string' | 'number' | 'date' | 'static-select';
|
7
|
+
}
|
8
|
+
|
9
|
+
export interface StaticSelectFilter extends Filter {
|
10
|
+
type: 'static-select';
|
11
|
+
options: { value: string; label: string }[];
|
12
|
+
}
|
4
13
|
|
5
14
|
export interface CellOptions {
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
15
|
+
name?: string;
|
16
|
+
title?: string;
|
17
|
+
type?: 'string' | 'date' | 'image' | 'number';
|
18
|
+
placeHolder?: string;
|
19
|
+
filter?: Filter | StaticSelectFilter;
|
10
20
|
}
|
11
21
|
|
12
22
|
export function Cell(options?: CellOptions): PropertyDecorator {
|
13
|
-
|
14
|
-
|
15
|
-
|
23
|
+
return (target, propertyKey) => {
|
24
|
+
const existingCells: string[] = Reflect.getMetadata(CELL_KEY, target) || [];
|
25
|
+
Reflect.defineMetadata(CELL_KEY, [...existingCells, propertyKey.toString()], target);
|
16
26
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
27
|
+
if (options) {
|
28
|
+
const keyString = `${CELL_KEY.toString()}:${propertyKey.toString()}:options`;
|
29
|
+
Reflect.defineMetadata(keyString, options, target);
|
30
|
+
}
|
31
|
+
};
|
22
32
|
}
|
@@ -1,17 +1,32 @@
|
|
1
|
-
import
|
1
|
+
import 'reflect-metadata';
|
2
2
|
|
3
|
-
const LIST_KEY =
|
3
|
+
const LIST_KEY = 'List';
|
4
4
|
|
5
|
-
export interface
|
5
|
+
export interface ListHeaderOptions {
|
6
|
+
title?: string;
|
7
|
+
create?: { path: string; label: string };
|
8
|
+
}
|
9
|
+
|
10
|
+
export interface ListCellOptions<T> {
|
11
|
+
details?: { path: string; label: string };
|
12
|
+
edit?: { path: string; label: string };
|
13
|
+
delete?: { label: string };
|
14
|
+
}
|
15
|
+
|
16
|
+
export interface ListOptions<T> {
|
17
|
+
headers?: ListHeaderOptions;
|
18
|
+
cells?: ((item: T) => ListCellOptions<T>) | ListCellOptions<T>;
|
19
|
+
}
|
6
20
|
|
7
|
-
export function List(options?: ListOptions): ClassDecorator {
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
21
|
+
export function List<T>(options?: ListOptions<T> | ((item: T) => ListOptions<T>)): ClassDecorator {
|
22
|
+
return (target: Function) => {
|
23
|
+
if (options) {
|
24
|
+
Reflect.defineMetadata(LIST_KEY, options, target);
|
25
|
+
}
|
26
|
+
};
|
13
27
|
}
|
14
28
|
|
15
|
-
export function getClassListData(entityClass:
|
16
|
-
|
29
|
+
export function getClassListData<T>(entityClass: T): ListOptions<T> | undefined {
|
30
|
+
//TODO: try to remove any
|
31
|
+
return Reflect.getMetadata(LIST_KEY, entityClass as any);
|
17
32
|
}
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import { ListOptions } from
|
2
|
-
import { CellOptions } from
|
1
|
+
import { ListOptions } from './List';
|
2
|
+
import { CellOptions } from './Cell';
|
3
3
|
|
4
|
-
export interface ListData {
|
5
|
-
|
6
|
-
|
4
|
+
export interface ListData<T> {
|
5
|
+
list?: ListOptions<T>;
|
6
|
+
cells: CellOptions[];
|
7
7
|
}
|
@@ -1,10 +1,10 @@
|
|
1
|
-
import { getClassListData } from
|
2
|
-
import { getCellFields } from
|
3
|
-
import { ListData } from
|
1
|
+
import { getClassListData } from './List';
|
2
|
+
import { getCellFields } from './GetCellFields';
|
3
|
+
import { ListData } from './ListData';
|
4
4
|
|
5
|
-
export function getListFields<T>(entityClass: T): ListData {
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
export function getListFields<T>(entityClass: T): ListData<T> {
|
6
|
+
return {
|
7
|
+
list: getClassListData(entityClass),
|
8
|
+
cells: getCellFields(entityClass),
|
9
|
+
};
|
10
10
|
}
|
package/src/index.ts
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
export type { OnSubmitFN, GetDetailsDataFN } from './components/pages/FormPage';
|
2
|
-
export type { GetDataForList } from './components/pages/ListPage';
|
3
|
-
export type { PaginatedResponse, PaginationParams } from './components/pages/ListPage';
|
4
2
|
export type { InitPanelOptions } from './types/initPanelOptions';
|
5
3
|
export type { ScreenCreatorData } from './types/ScreenCreatorData';
|
6
4
|
export type { OnLogin } from './components/pages/Login';
|
7
5
|
export type { AnyClass } from './types/AnyClass';
|
6
|
+
export type {
|
7
|
+
GetDataForList,
|
8
|
+
PaginatedResponse,
|
9
|
+
GetDataParams,
|
10
|
+
} from './components/components/list/ListPage';
|
8
11
|
|
9
|
-
export { ListPage } from './components/
|
12
|
+
export { ListPage } from './components/components/list/ListPage';
|
10
13
|
export { FormPage } from './components/pages/FormPage';
|
11
14
|
export type { FormPageProps } from './components/pages/FormPage';
|
12
15
|
export { Login } from './components/pages/Login';
|
@@ -21,3 +24,5 @@ export { Crud } from './decorators/Crud';
|
|
21
24
|
export { Cell } from './decorators/list/Cell';
|
22
25
|
export { Input } from './decorators/form/Input';
|
23
26
|
// Export page components
|
27
|
+
export { getFormFields } from './decorators/form/getFormFields';
|
28
|
+
export { getInputFields } from './decorators/form/Input';
|
@@ -0,0 +1,134 @@
|
|
1
|
+
.filter-popup {
|
2
|
+
position: absolute;
|
3
|
+
top: 60px; //TODO: make this variable $list-header-height
|
4
|
+
//TODO: make this variable
|
5
|
+
left: 260px;
|
6
|
+
right: 10px;
|
7
|
+
background: #1e1e1e;
|
8
|
+
padding: 20px;
|
9
|
+
border-radius: 8px;
|
10
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
11
|
+
border: 1px solid #333;
|
12
|
+
z-index: 10;
|
13
|
+
&-header {
|
14
|
+
display: flex;
|
15
|
+
justify-content: space-between;
|
16
|
+
align-items: center;
|
17
|
+
margin-bottom: 20px;
|
18
|
+
|
19
|
+
h3 {
|
20
|
+
margin: 0;
|
21
|
+
font-size: 1.2rem;
|
22
|
+
color: #ffffff;
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
&-content {
|
27
|
+
max-height: 400px;
|
28
|
+
overflow-y: auto;
|
29
|
+
|
30
|
+
&::-webkit-scrollbar {
|
31
|
+
width: 8px;
|
32
|
+
}
|
33
|
+
|
34
|
+
&::-webkit-scrollbar-track {
|
35
|
+
background: #2d2d2d;
|
36
|
+
border-radius: 4px;
|
37
|
+
}
|
38
|
+
|
39
|
+
&::-webkit-scrollbar-thumb {
|
40
|
+
background: #444;
|
41
|
+
border-radius: 4px;
|
42
|
+
|
43
|
+
&:hover {
|
44
|
+
background: #555;
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
&-footer {
|
50
|
+
display: flex;
|
51
|
+
justify-content: flex-end;
|
52
|
+
gap: 10px;
|
53
|
+
margin-top: 20px;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
.close-button {
|
58
|
+
background: none;
|
59
|
+
border: none;
|
60
|
+
font-size: 1.5rem;
|
61
|
+
cursor: pointer;
|
62
|
+
padding: 0;
|
63
|
+
color: #999;
|
64
|
+
transition: color 0.2s ease;
|
65
|
+
|
66
|
+
&:hover {
|
67
|
+
color: #ffffff;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
.filter-field {
|
72
|
+
margin-bottom: 15px;
|
73
|
+
|
74
|
+
label {
|
75
|
+
display: block;
|
76
|
+
margin-bottom: 5px;
|
77
|
+
font-size: 0.9rem;
|
78
|
+
color: #bbb;
|
79
|
+
}
|
80
|
+
|
81
|
+
input {
|
82
|
+
width: 100%;
|
83
|
+
padding: 10px;
|
84
|
+
border: 1px solid #444;
|
85
|
+
border-radius: 6px;
|
86
|
+
font-size: 0.9rem;
|
87
|
+
background: #2d2d2d;
|
88
|
+
color: #ffffff;
|
89
|
+
transition: all 0.2s ease;
|
90
|
+
|
91
|
+
&:focus {
|
92
|
+
border-color: #0066cc;
|
93
|
+
outline: none;
|
94
|
+
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
|
95
|
+
}
|
96
|
+
|
97
|
+
&::placeholder {
|
98
|
+
color: #666;
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
.cancel-button,
|
104
|
+
.apply-button {
|
105
|
+
padding: 10px 20px;
|
106
|
+
border-radius: 6px;
|
107
|
+
cursor: pointer;
|
108
|
+
font-size: 0.9rem;
|
109
|
+
transition: all 0.2s ease;
|
110
|
+
}
|
111
|
+
|
112
|
+
.cancel-button {
|
113
|
+
background: #2d2d2d;
|
114
|
+
border: 1px solid #444;
|
115
|
+
color: #bbb;
|
116
|
+
|
117
|
+
&:hover {
|
118
|
+
background: #333;
|
119
|
+
border-color: #555;
|
120
|
+
color: #fff;
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
.apply-button {
|
125
|
+
background: #0066cc;
|
126
|
+
border: 1px solid #0056b3;
|
127
|
+
color: white;
|
128
|
+
|
129
|
+
&:hover {
|
130
|
+
background: #0056b3;
|
131
|
+
transform: translateY(-1px);
|
132
|
+
box-shadow: 0 2px 8px rgba(0, 102, 204, 0.3);
|
133
|
+
}
|
134
|
+
}
|
package/src/styles/form.scss
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
.form-wrapper {
|
2
|
-
max-width: 400px;
|
3
2
|
padding: 20px;
|
4
|
-
border-radius: 10px;
|
5
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
6
3
|
background-color: #1a1a1a;
|
7
4
|
color: #f2f2f2;
|
5
|
+
width: 100%;
|
8
6
|
|
9
7
|
form {
|
10
8
|
display: flex;
|
@@ -59,7 +57,7 @@
|
|
59
57
|
|
60
58
|
select {
|
61
59
|
appearance: none;
|
62
|
-
background-image: url(
|
60
|
+
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
|
63
61
|
background-repeat: no-repeat;
|
64
62
|
background-position: right 0.7rem top 50%;
|
65
63
|
background-size: 0.65rem auto;
|
package/src/styles/index.scss
CHANGED
@@ -1,13 +1,15 @@
|
|
1
|
-
@
|
2
|
-
@
|
3
|
-
@
|
4
|
-
@
|
5
|
-
@
|
6
|
-
@
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
1
|
+
@import './layout';
|
2
|
+
@import './sidebar';
|
3
|
+
@import './form';
|
4
|
+
@import './list';
|
5
|
+
@import './login';
|
6
|
+
@import './error-boundary';
|
7
|
+
@import './image-uploader';
|
8
|
+
@import './counter';
|
9
|
+
@import './loading-screen';
|
10
|
+
@import './pagination';
|
11
|
+
@import './filter-popup';
|
12
|
+
@import './uploader';
|
11
13
|
|
12
14
|
.layout {
|
13
15
|
display: flex;
|
@@ -31,16 +33,10 @@
|
|
31
33
|
*::after {
|
32
34
|
box-sizing: border-box;
|
33
35
|
}
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
@import './error-boundary';
|
41
|
-
@import './image-uploader';
|
42
|
-
@import './counter';
|
43
|
-
@import './loading-screen';
|
44
|
-
@import './pagination';
|
45
|
-
@import './scrollbar';
|
36
|
+
}
|
37
|
+
.icon {
|
38
|
+
width: 14px;
|
39
|
+
height: 14px;
|
40
|
+
stroke: currentColor;
|
41
|
+
fill: currentColor;
|
46
42
|
}
|