proje-react-panel 1.0.0 → 1.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.
Files changed (39) hide show
  1. package/.idea/modules.xml +1 -1
  2. package/.idea/vcs.xml +3 -1
  3. package/dist/bundle.css +128 -0
  4. package/dist/{list → components/list}/List.d.ts +2 -3
  5. package/dist/index.cjs.js +12 -1
  6. package/dist/index.d.ts +6 -1
  7. package/dist/index.esm.js +12 -1
  8. package/dist/screens/ControllerCreate.d.ts +0 -1
  9. package/dist/{getFields.d.ts → utils/getFields.d.ts} +1 -1
  10. package/dist/{storeData.d.ts → utils/storeData.d.ts} +1 -1
  11. package/package.json +9 -5
  12. package/src/api/crudApi.ts +16 -0
  13. package/src/components/Panel.tsx +13 -0
  14. package/src/components/layout/Layout.tsx +18 -0
  15. package/src/components/layout/SideBar.tsx +23 -0
  16. package/src/components/list/List.tsx +75 -0
  17. package/src/declerations/Cell.ts +37 -0
  18. package/src/declerations/Crud.ts +20 -0
  19. package/src/index.ts +6 -0
  20. package/src/screens/ControllerCreate.tsx +13 -0
  21. package/src/screens/ControllerDetails.tsx +34 -0
  22. package/src/screens/ControllerEdit.tsx +31 -0
  23. package/src/screens/ControllerList.tsx +40 -0
  24. package/src/screens/Form.tsx +67 -0
  25. package/src/styles/form.scss +58 -0
  26. package/src/styles/index.scss +4 -0
  27. package/src/styles/layout.scss +13 -0
  28. package/src/styles/list.scss +39 -0
  29. package/src/styles/sidebar.scss +76 -0
  30. package/src/types/Screen.ts +4 -0
  31. package/src/types/ScreenCreatorData.ts +9 -0
  32. package/src/utils/createScreens.ts +5 -0
  33. package/src/utils/getFields.ts +21 -0
  34. package/src/utils/getScreenForRoutes.tsx +44 -0
  35. package/src/utils/storeData.ts +7 -0
  36. /package/.idea/{react-panel.iml → proje-react-panel.iml} +0 -0
  37. /package/dist/{Panel.d.ts → components/Panel.d.ts} +0 -0
  38. /package/dist/{createScreens.d.ts → utils/createScreens.d.ts} +0 -0
  39. /package/dist/{getScreenForRoutes.d.ts → utils/getScreenForRoutes.d.ts} +0 -0
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
- import '../styles/form.scss';
3
2
  import { Screen } from '../types/Screen';
4
3
  export declare function ControllerCreate({ screen }: {
5
4
  screen: Screen;
@@ -1,2 +1,2 @@
1
- import { ScreenCreatorData } from "./types/ScreenCreatorData";
1
+ import { ScreenCreatorData } from "../types/ScreenCreatorData";
2
2
  export declare function getFields<T>(entityClass: T): ScreenCreatorData<T>;
@@ -1,4 +1,4 @@
1
- import { ScreenCreatorData } from "./types/ScreenCreatorData";
1
+ import { ScreenCreatorData } from "../types/ScreenCreatorData";
2
2
  export declare const StoreData: {
3
3
  screens: Record<string, ScreenCreatorData<any>>;
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proje-react-panel",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "author": "SEFA DEMİR",
6
6
  "license": "ISC",
@@ -14,17 +14,18 @@
14
14
  "scripts": {
15
15
  "test": "echo \"Error: no test specified\" && exit 1",
16
16
  "build": "rollup -c ",
17
- "lint": "eslint src --ext .ts,.tsx"
17
+ "lint": "eslint src --ext .ts,.tsx",
18
+ "generate-nestjs-entity": "node scripts/generate-nestjs-entity.js"
18
19
  },
19
20
  "repository": {
20
21
  "type": "git",
21
- "url": "git+https://github.com/demirsefa/react-panel.git"
22
+ "url": "git+https://github.com/demirsefa/proje-react-panel.git"
22
23
  },
23
24
  "keywords": [],
24
25
  "bugs": {
25
- "url": "https://github.com/demirsefa/react-panel/issues"
26
+ "url": "https://github.com/demirsefa/proje-react-panel/issues"
26
27
  },
27
- "homepage": "https://github.com/demirsefa/react-panel#readme",
28
+ "homepage": "https://github.com/demirsefa/proje-react-panel#readme",
28
29
  "dependencies": {
29
30
  "axios": "^1.8.3",
30
31
  "react-hook-form": "^7.54.2",
@@ -33,6 +34,7 @@
33
34
  "devDependencies": {
34
35
  "@hookform/resolvers": "^4.1.3",
35
36
  "@rollup/plugin-commonjs": "^28.0.3",
37
+ "@rollup/plugin-json": "^6.1.0",
36
38
  "@rollup/plugin-node-resolve": "^16.0.1",
37
39
  "@types/react": "^19.0.10",
38
40
  "@types/react-dom": "^19.0.4",
@@ -41,11 +43,13 @@
41
43
  "class-transformer": "^0.5.1",
42
44
  "class-validator": "^0.14.1",
43
45
  "eslint": "^8.57.1",
46
+ "node-sass": "^9.0.0",
44
47
  "react": "^19.0.0",
45
48
  "react-router-dom": "^7.3.0",
46
49
  "reflect-metadata": "^0.2.2",
47
50
  "rollup": "^4.35.0",
48
51
  "rollup-plugin-peer-deps-external": "^2.2.4",
52
+ "rollup-plugin-scss": "^4.0.1",
49
53
  "rollup-plugin-terser": "^7.0.2",
50
54
  "rollup-plugin-typescript2": "^0.36.0",
51
55
  "typescript": "^5.8.2"
@@ -0,0 +1,16 @@
1
+ import axios from 'axios';
2
+
3
+ export const CrudApi = {
4
+ getList: (api: string, page: number) => {
5
+ return axios.get(api, { data: { page } });
6
+ },
7
+ create: (api: string, data: any) => {
8
+ return axios.post(api, data);
9
+ },
10
+ details(api: string, id: any) {
11
+ return axios.get(api + '/' + id);
12
+ },
13
+ edit(api: string, data: any) {
14
+ return axios.put(api + '/' + data.id, data);
15
+ },
16
+ };
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+
3
+ type AppProps = {
4
+ children: React.ReactNode;
5
+ };
6
+
7
+ export function Panel({ children }: AppProps) {
8
+ return (
9
+ <>
10
+ {children}
11
+ </>
12
+ );
13
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { Outlet } from 'react-router-dom';
3
+ import { SideBar } from './SideBar';
4
+
5
+ export function Layout({
6
+ children,
7
+ noSidebar = false,
8
+ }: {
9
+ children?: React.ReactNode;
10
+ noSidebar?: boolean;
11
+ }) {
12
+ return (
13
+ <div className="layout">
14
+ {!noSidebar && <SideBar />}
15
+ <main className="content">{children || <Outlet />}</main>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,23 @@
1
+ import React, { useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+
4
+
5
+ export function SideBar() {
6
+ const [isOpen, setIsOpen] = useState(true);
7
+
8
+ return (
9
+ <div className={`sidebar ${isOpen ? 'open' : 'closed'}`}>
10
+ <button className='toggle-button' onClick={() => setIsOpen(!isOpen)}>
11
+ {isOpen ? '<' : '>'}
12
+ </button>
13
+ <nav className='nav-links'>
14
+ <Link to='/'>Home</Link>
15
+ <Link to='/accounts'>Accounts</Link>
16
+ <Link to='/maps'>Maps</Link>
17
+ <div className='bottom-link'>
18
+ <Link to='/settings'>Settings</Link>
19
+ </div>
20
+ </nav>
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+ import { CellOptions } from '../../declerations/Cell';
3
+ import { Link } from 'react-router-dom';
4
+ import { Screen } from '../../types/Screen';
5
+
6
+ interface ListProps<T> {
7
+ data: T[];
8
+ cells: CellOptions<T>[];
9
+ screen: Screen;
10
+ }
11
+
12
+ export function List<T>({ data, cells, screen }: ListProps<T>) {
13
+ if (!data || data.length === 0) {
14
+ return <div>No items available</div>;
15
+ }
16
+
17
+ return (
18
+ <div className='list-wrapper'>
19
+ <div className='header'>List</div>
20
+ <table className='list-table'>
21
+ <thead>
22
+ <tr>
23
+ {cells.map((cellOptions) => (
24
+ <th key={cellOptions.name}>{cellOptions.title ?? cellOptions.name}</th>
25
+ ))}
26
+ <th />
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ {data.map((item, index) => (
31
+ <tr key={index}>
32
+ {cells.map((cellOptions) => {
33
+ // @ts-ignore
34
+ const value = item[cellOptions.name];
35
+ let formattedValue = value ?? '-'; // Default value if the field is undefined or null
36
+
37
+ switch (cellOptions.type) {
38
+ case 'date':
39
+ if (value) {
40
+ const date = new Date(value);
41
+ formattedValue = `${date.getDate().toString().padStart(2, '0')}/${(date.getMonth() + 1)
42
+ .toString()
43
+ .padStart(2, '0')}/${date.getFullYear()} ${date.getHours().toString().padStart(2, '0')}:${date
44
+ .getMinutes()
45
+ .toString()
46
+ .padStart(2, '0')}`;
47
+ }
48
+ break;
49
+
50
+ case 'number':
51
+ case 'string':
52
+ default:
53
+ formattedValue = value ? value.toString() : (cellOptions?.placeHolder ?? '-'); // Handles string type or default fallback
54
+ break;
55
+ }
56
+ let render = formattedValue;
57
+ if (cellOptions.linkTo) {
58
+ render = <Link to={cellOptions.linkTo(item)}>{formattedValue}</Link>;
59
+ }
60
+ return <td key={cellOptions.name}>{render}</td>;
61
+ })}
62
+ <td>
63
+ {/*@ts-ignore*/}
64
+ <Link to={'edit/' + (item?.id ?? '-')}>Edit</Link>
65
+ {/*@ts-ignore*/}
66
+ <Link to={'details/' + (item?.id ?? '-')}>Details</Link>
67
+ </td>
68
+ </tr>
69
+ ))}
70
+
71
+ </tbody>
72
+ </table>
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,37 @@
1
+ import 'reflect-metadata';
2
+
3
+ const CELL_KEY = Symbol('cell');
4
+
5
+ export interface CellOptions<T> {
6
+ name?: string;
7
+ title?: string;
8
+ type?: 'string' | 'number' | 'date';
9
+ placeHolder?: string;
10
+ linkTo?: (item: T) => string;
11
+ }
12
+
13
+ export function Cell<T>(options?: CellOptions<T>): PropertyDecorator {
14
+ return (target, propertyKey) => {
15
+ const existingCells: string[] = Reflect.getMetadata(CELL_KEY, target) || [];
16
+ Reflect.defineMetadata(CELL_KEY, [...existingCells, propertyKey.toString()], target);
17
+
18
+ if (options) {
19
+ const keyString = `${CELL_KEY.toString()}:${propertyKey.toString()}:options`;
20
+ Reflect.defineMetadata(keyString, options, target);
21
+ }
22
+ };
23
+ }
24
+
25
+
26
+ export function getCellFields<T>(entityClass: any): CellOptions<T> [] {
27
+ const prototype = entityClass.prototype;
28
+ const cellFields: string[] = Reflect.getMetadata(CELL_KEY, prototype) || [];
29
+ return cellFields.map((field) => {
30
+ const fields = (Reflect.getMetadata(`${CELL_KEY.toString()}:${field}:options`, prototype) || {});
31
+ return {
32
+ ...fields,
33
+ name: fields?.name ?? field,
34
+ };
35
+
36
+ });
37
+ }
@@ -0,0 +1,20 @@
1
+ import 'reflect-metadata';
2
+
3
+ const CRUD_KEY = 'Crud'; // Changed from Symbol to string
4
+
5
+ export interface CrudOptions {
6
+ controller: string;
7
+ }
8
+
9
+ export function Crud(options?: CrudOptions): ClassDecorator {
10
+ return (target: Function) => {
11
+ if (options) {
12
+ Reflect.defineMetadata(CRUD_KEY, options, target);
13
+ }
14
+ };
15
+ }
16
+
17
+
18
+ export function getClassCrudData(entityClass: any): CrudOptions | undefined {
19
+ return Reflect.getMetadata(CRUD_KEY, entityClass);
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import "./styles/index.scss"
2
+ export { createScreens } from "./utils/createScreens";
3
+ export { getScreenForRoutes } from "./utils/getScreenForRoutes";
4
+ export { Layout } from "./components/layout/Layout";
5
+ export { SideBar } from "./components/layout/SideBar";
6
+ export {Panel} from "./components/Panel"
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { Screen } from '../types/Screen';
3
+ import { Layout } from '../components/layout/Layout';
4
+ import { Form } from './Form';
5
+
6
+ export function ControllerCreate({ screen }: { screen: Screen }) {
7
+
8
+ return (
9
+ <Layout>
10
+ <Form screen={screen} />
11
+ </Layout>
12
+ );
13
+ }
@@ -0,0 +1,34 @@
1
+ import { Layout } from '../components/layout/Layout';
2
+ import { useParams } from 'react-router-dom';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { CrudApi } from '../api/crudApi';
5
+ import { Screen } from '../types/Screen';
6
+
7
+ export function ControllerDetails({ screen }: { screen: Screen }) {
8
+ const { id } = useParams();
9
+ const [data, setData] = useState<any>(null);
10
+ const [error, setError] = useState(null);
11
+
12
+ useEffect(() => {
13
+ if (screen.controller && id) {
14
+ CrudApi.details(screen.controller, id)
15
+ .then((res) => {
16
+ setData(res.data);
17
+ })
18
+ .catch((e: any) => {
19
+ setError(e);
20
+ console.error(e);
21
+ });
22
+ }
23
+ }, [id, screen]);
24
+
25
+ return (
26
+ <Layout>
27
+ <p
28
+ dangerouslySetInnerHTML={{
29
+ __html: JSON.stringify(data, null, ' ' + '<br/>'),
30
+ }}
31
+ />
32
+ </Layout>
33
+ );
34
+ }
@@ -0,0 +1,31 @@
1
+ import { Layout } from '../components/layout/Layout';
2
+ import { Form } from './Form';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { Screen } from '../types/Screen';
5
+ import { useParams } from 'react-router-dom';
6
+ import { CrudApi } from '../api/crudApi';
7
+
8
+ export function ControllerEdit({ screen }: { screen: Screen }) {
9
+ const { id } = useParams();
10
+ const [data, setData] = useState<any>(null);
11
+ const [error, setError] = useState(null);
12
+
13
+ useEffect(() => {
14
+ if (screen.controller && id) {
15
+ CrudApi.details(screen.controller, id)
16
+ .then((res) => {
17
+ setData(res.data);
18
+ })
19
+ .catch((e: any) => {
20
+ setError(e);
21
+ console.error(e);
22
+ });
23
+ }
24
+ }, [id, screen]);
25
+
26
+ return (
27
+ <Layout>
28
+ <Form data={data} screen={screen} />
29
+ </Layout>
30
+ );
31
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { Layout } from '../components/layout/Layout';
3
+ import { Screen } from '../types/Screen';
4
+ import { useEffect, useState } from 'react';
5
+ import { CrudApi } from '../api/crudApi';
6
+ import { Link } from 'react-router-dom';
7
+ import { List } from '../components/list/List';
8
+
9
+ import { StoreData } from "../utils/storeData";
10
+
11
+ export function ControllerList({ screen }: { screen: Screen }) {
12
+ const [page, setPage] = useState(0);
13
+ const [data, setData] = useState<any>(null);
14
+ const [error, setError] = useState(null);
15
+
16
+ useEffect(() => {
17
+ if (screen.controller) {
18
+ CrudApi.getList(screen.controller, page)
19
+ .then((res) => {
20
+ setData(res.data);
21
+ })
22
+ .catch((e: any) => {
23
+ setError(e);
24
+ console.error(e);
25
+ });
26
+ }
27
+ }, [page, screen.controller]);
28
+
29
+ return (
30
+ <Layout>
31
+ <Link to={'/maps/create'}>Create</Link>
32
+ {error ? <p>Error {error}</p> : null}
33
+ <List
34
+ screen={screen}
35
+ cells={StoreData.screens[screen.key].cells}
36
+ data={data}
37
+ />
38
+ </Layout>
39
+ );
40
+ }
@@ -0,0 +1,67 @@
1
+ import React, { useEffect } from 'react';
2
+ import { Screen } from '../types/Screen';
3
+ import { FieldErrors, useForm } from 'react-hook-form';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import { CrudApi } from '../api/crudApi';
6
+ import { StoreData } from "../utils/storeData";
7
+
8
+ export function Form({ data, screen }: { data?: any; screen: Screen }) {
9
+ const {
10
+ register,
11
+ handleSubmit,
12
+ reset,
13
+ formState: { errors },
14
+ } = useForm<any>({
15
+ resolver: StoreData.screens[screen.controller].resolver,
16
+ defaultValues: data,
17
+ });
18
+ const navigate = useNavigate();
19
+ const fields = StoreData.screens[screen.controller].fields;
20
+ useEffect(() => {
21
+ reset(data);
22
+ }, [data, reset]);
23
+ return (
24
+ <div className="form-wrapper">
25
+ <form
26
+ onSubmit={handleSubmit((dataForm) => {
27
+ if (data) {
28
+ CrudApi.edit(screen.controller, dataForm).then(() => {
29
+ navigate('/' + screen.controller, {
30
+ replace: true,
31
+ });
32
+ });
33
+ } else {
34
+ CrudApi.create(screen.controller, dataForm).then(() => {
35
+ navigate('/' + screen.controller, {
36
+ replace: true,
37
+ });
38
+ });
39
+ }
40
+ })}
41
+ >
42
+ {fields.map((field) => (
43
+ <div className="form-field" key={field}>
44
+ <label htmlFor={field}>
45
+ {field.charAt(0).toUpperCase() + field.slice(1)}
46
+ </label>
47
+ <input
48
+ type="text"
49
+ {...register(field)}
50
+ placeholder={`Enter ${field}`}
51
+ id={field}
52
+ />
53
+ {errors[field] && (
54
+ <span className="error-message">
55
+ {/*@ts-ignore*/}
56
+ {(errors[field] as FieldErrors)?.message}
57
+ </span>
58
+ )}
59
+ </div>
60
+ ))}
61
+ <button type="submit" className="submit-button">
62
+ Submit
63
+ </button>
64
+ </form>
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,58 @@
1
+ .form-wrapper {
2
+ max-width: 400px;
3
+ padding: 20px;
4
+ background-color: #f9f9f9;
5
+ border-radius: 10px;
6
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
7
+
8
+ form {
9
+ display: flex;
10
+ flex-direction: column;
11
+ }
12
+
13
+ .form-field {
14
+ margin-bottom: 16px;
15
+
16
+ label {
17
+ font-weight: bold;
18
+ margin-bottom: 6px;
19
+ display: block;
20
+ color: #333;
21
+ }
22
+
23
+ input[type='text'] {
24
+ padding: 10px;
25
+ font-size: 16px;
26
+ border: 1px solid #ccc;
27
+ border-radius: 5px;
28
+ width: 100%;
29
+ transition: border-color 0.2s;
30
+
31
+ &:focus {
32
+ border-color: #007bff;
33
+ outline: none;
34
+ }
35
+ }
36
+
37
+ .error-message {
38
+ margin-top: 4px;
39
+ font-size: 14px;
40
+ color: #ff4d4f;
41
+ }
42
+ }
43
+
44
+ .submit-button {
45
+ padding: 12px;
46
+ font-size: 18px;
47
+ background-color: #007bff;
48
+ color: white;
49
+ border: none;
50
+ border-radius: 5px;
51
+ cursor: pointer;
52
+ transition: background-color 0.2s;
53
+
54
+ &:hover {
55
+ background-color: #0056b3;
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,4 @@
1
+ @import 'sidebar';
2
+ @import 'form';
3
+ @import 'layout';
4
+ @import 'list';
@@ -0,0 +1,13 @@
1
+
2
+ .layout {
3
+ display: flex;
4
+ min-height: 100vh;
5
+ width: 100%;
6
+
7
+ .content {
8
+ flex: 1;
9
+ padding: 1rem;
10
+ overflow-y: auto;
11
+ transition: margin-left 0.3s ease;
12
+ }
13
+ }
@@ -0,0 +1,39 @@
1
+ .list-wrapper {
2
+ width: 100%;
3
+ padding: 16px;
4
+ background-color: #ffffff;
5
+ border-radius: 8px;
6
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
7
+
8
+ .header {
9
+ font-size: 24px;
10
+ font-weight: bold;
11
+ text-align: center;
12
+ margin-bottom: 20px;
13
+ }
14
+
15
+ .list-table {
16
+ width: 100%;
17
+ border-collapse: collapse;
18
+
19
+ th,
20
+ td {
21
+ padding: 12px 16px;
22
+ text-align: left;
23
+ border-bottom: 1px solid #ddd;
24
+ }
25
+
26
+ th {
27
+ background-color: #007bff;
28
+ color: white;
29
+ }
30
+
31
+ tr:nth-child(even) {
32
+ background-color: #f9f9f9;
33
+ }
34
+
35
+ tr:hover {
36
+ background-color: #e8f4ff;
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,76 @@
1
+ .sidebar {
2
+ position: relative;
3
+ background-color: #f5f5f5;
4
+ height: 100vh;
5
+ transition: width 0.3s ease;
6
+ border-right: 1px solid #e0e0e0;
7
+
8
+ &.open {
9
+ width: 250px;
10
+ }
11
+
12
+ &.closed {
13
+ width: 60px;
14
+
15
+ .nav-links {
16
+ a {
17
+ span {
18
+ display: none;
19
+ }
20
+ }
21
+ }
22
+ }
23
+
24
+ .toggle-button {
25
+ position: absolute;
26
+ top: 10px;
27
+ right: -12px;
28
+ width: 24px;
29
+ height: 24px;
30
+ border-radius: 50%;
31
+ background-color: #ffffff;
32
+ border: 1px solid #e0e0e0;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ cursor: pointer;
37
+ z-index: 10;
38
+
39
+ &:hover {
40
+ background-color: #f0f0f0;
41
+ }
42
+ }
43
+
44
+ .nav-links {
45
+ display: flex;
46
+ flex-direction: column;
47
+ padding: 2rem 1rem;
48
+ height: 100%;
49
+
50
+ a {
51
+ padding: 0.75rem 1rem;
52
+ margin-bottom: 0.5rem;
53
+ color: #333;
54
+ text-decoration: none;
55
+ border-radius: 4px;
56
+ font-weight: 500;
57
+
58
+ &:hover {
59
+ background-color: rgba(0, 0, 0, 0.05);
60
+ }
61
+
62
+ &.active {
63
+ background-color: rgba(0, 0, 0, 0.1);
64
+ }
65
+ }
66
+
67
+ .bottom-link {
68
+ margin-top: auto;
69
+
70
+ a {
71
+ color: #666;
72
+ font-weight: 400;
73
+ }
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,4 @@
1
+ export interface Screen {
2
+ key: string,
3
+ controller: string,
4
+ }