├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── demo.gif
├── package-lock.json
├── package.json
├── public
└── index.html
├── src
├── App.css
├── App.tsx
├── app
│ └── store.ts
├── features
│ ├── counter
│ │ ├── Decrementor.css
│ │ ├── Decrementor.tsx
│ │ ├── Incrementor.css
│ │ ├── Incrementor.tsx
│ │ ├── Watcher.css
│ │ ├── Watcher.tsx
│ │ └── counterSlice.ts
│ └── widgets
│ │ ├── Lumino.css
│ │ ├── Lumino.tsx
│ │ └── widgetsSlice.ts
├── index.css
├── index.tsx
└── react-app-env.d.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lumino Widgets + React/Redux
2 |
3 | 
4 |
5 | This project is an example on how to use `@lumino/widgets` in a React application. Inspired by [vue-lumino](https://github.com/tupilabs/vue-lumino).
6 |
7 | The goal was to have a window management similar to VSCode's, but keep access to Redux State and add new windows via Redux actions. In order to realise this we have keep in mind, that `lumino` (formerly `phosphorjs`) manages the windows outside of React.
8 |
9 | All the magic happens in `Lumino.tsx` & `widgetsSlice.ts`.
10 |
11 | Steps to reproduce:
12 | - Decide what will be a widget in your app, define props for these widgets
13 | - In my case its `{id: string, name: string}`, where the ID is used to add/remove a specific widget and the name will be displayed in the tab.
14 | - Define a redux state for the widgets
15 | - I wrote my Redux widget handling in `widgetsSlice.ts` using the modern `@redux/toolkit` which I love. Each supported widget has its own type that will be used for telling lumino which React component to render.
16 | - Inherit from luminos `Widget` class
17 | - We use our custom class to handle communication between lumino and React through custom events.
18 | - Initialize luminos `Boxpanel` and `DockPanel` during the first render in a `useEffect` hook. Attach callbacks to the elements custom events to communicate our custom widgets events back to react.
19 | - Watch the redux widgetstate and add a new widget to the `DockPanel` when a new widget appears in the state.
20 |
21 | ## Styling
22 |
23 | To my opinion custom styling is a rather difficult process when using lumino. But with your browsers DevTools it should be okay. For this demo I replaced the close icon with an "X" and changed the window overlay preview to be blue. Change them add your styles in `Lumino.css`.
24 |
25 | ## Will there be a package?
26 |
27 | I have no plans on making this a package, since this is an early proof of concept that will be the base for a production app. Also I never created a React package and might need some advice. So I decided to leave this a template with instructions on how to integrate `@lumino/widgets` into your react app.
28 |
29 | ## Contributing
30 |
31 | I am happy for any feedback or better ideas on how to improve the integration of `@lumino/widgets` into React/Redux. Feel free to file an Issue or a PR :)
32 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ccssmnn/lumino-react/0babd3689e47c2c8784bd6bc6b3dea00ba4daf1c/demo.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lumino-react",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-free": "^5.15.2",
7 | "@lumino/default-theme": "^0.8.0",
8 | "@lumino/widgets": "^1.17.0",
9 | "@reduxjs/toolkit": "^1.5.0",
10 | "@testing-library/jest-dom": "^4.2.4",
11 | "@testing-library/react": "^9.5.0",
12 | "@testing-library/user-event": "^7.2.1",
13 | "@types/jest": "^24.9.1",
14 | "@types/node": "^12.19.14",
15 | "@types/react": "^16.14.2",
16 | "@types/react-dom": "^16.9.10",
17 | "@types/react-redux": "^7.1.15",
18 | "@types/redux-logger": "^3.0.8",
19 | "react": "^17.0.1",
20 | "react-dom": "^17.0.1",
21 | "react-redux": "^7.2.2",
22 | "react-scripts": "4.0.1",
23 | "redux-logger": "^3.0.6",
24 | "typescript": "^3.8.3"
25 | },
26 | "scripts": {
27 | "start": "react-scripts start",
28 | "build": "react-scripts build",
29 | "test": "react-scripts test",
30 | "eject": "react-scripts eject"
31 | },
32 | "eslintConfig": {
33 | "extends": "react-app"
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Lumino Widgets & React Redux
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | button {
2 | height: 48px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Lumino from "./features/widgets/Lumino";
3 | import { useAppDispatch } from "./app/store";
4 | import {
5 | addIncrementor,
6 | addDecrementor,
7 | addWatcher,
8 | } from "./features/widgets/widgetsSlice";
9 |
10 | import "./App.css";
11 |
12 | function App() {
13 | const dispatch = useAppDispatch();
14 | return (
15 |
16 | dispatch(addIncrementor())}>
17 | Add Incrementor!
18 |
19 | dispatch(addDecrementor())}>
20 | Add Decrementor!
21 |
22 | dispatch(addWatcher())}>Add Watcher!
23 |
24 |
25 | );
26 | }
27 |
28 | export default App;
29 |
--------------------------------------------------------------------------------
/src/app/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
2 | import counterReducer from "../features/counter/counterSlice";
3 | import widgetsReducer from "../features/widgets/widgetsSlice";
4 | import { useDispatch } from "react-redux";
5 | import logger from "redux-logger";
6 |
7 | export const store = configureStore({
8 | reducer: {
9 | counter: counterReducer,
10 | widgets: widgetsReducer,
11 | },
12 | middleware: [logger],
13 | });
14 |
15 | export const useAppDispatch = () => useDispatch();
16 |
17 | export type RootState = ReturnType;
18 | export type AppThunk = ThunkAction<
19 | ReturnType,
20 | RootState,
21 | unknown,
22 | Action
23 | >;
24 |
--------------------------------------------------------------------------------
/src/features/counter/Decrementor.css:
--------------------------------------------------------------------------------
1 | .decrementor {
2 | font-size: large;
3 | position: absolute;
4 | top: 50%;
5 | left: 50%;
6 | transform: translate(-50%, -50%);
7 | }
8 |
--------------------------------------------------------------------------------
/src/features/counter/Decrementor.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { decrement } from "./counterSlice";
3 | import "./Decrementor.css";
4 | import { useAppDispatch } from "../../app/store";
5 | import { ReactWidget } from "../widgets/Lumino";
6 |
7 | const Decrementor: ReactWidget = () => {
8 | const dispatch = useAppDispatch();
9 | return (
10 |
11 | dispatch(decrement())}>Decrement Count
12 |
13 | );
14 | };
15 |
16 | export default Decrementor;
17 |
--------------------------------------------------------------------------------
/src/features/counter/Incrementor.css:
--------------------------------------------------------------------------------
1 | .incrementor {
2 | font-size: large;
3 | position: absolute;
4 | top: 50%;
5 | left: 50%;
6 | transform: translate(-50%, -50%);
7 | }
8 |
--------------------------------------------------------------------------------
/src/features/counter/Incrementor.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { increment } from "./counterSlice";
3 | import "./Incrementor.css";
4 | import { useAppDispatch } from "../../app/store";
5 | import { ReactWidget } from "../widgets/Lumino";
6 |
7 | const Incrementor: ReactWidget = () => {
8 | const dispatch = useAppDispatch();
9 | return (
10 |
11 | dispatch(increment())}>Increment Count
12 |
13 | );
14 | };
15 |
16 | export default Incrementor;
17 |
--------------------------------------------------------------------------------
/src/features/counter/Watcher.css:
--------------------------------------------------------------------------------
1 | .watcher {
2 | font-size: large;
3 | position: absolute;
4 | top: 50%;
5 | left: 50%;
6 | transform: translate(-50%, -50%);
7 | }
8 |
--------------------------------------------------------------------------------
/src/features/counter/Watcher.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { selectCount } from "./counterSlice";
3 | import { useSelector } from "react-redux";
4 |
5 | import "./Watcher.css";
6 | import { ReactWidget } from "../widgets/Lumino";
7 |
8 | const Watcher: ReactWidget = () => {
9 | const count = useSelector(selectCount);
10 | return The current count is {count}
;
11 | };
12 |
13 | export default Watcher;
14 |
--------------------------------------------------------------------------------
/src/features/counter/counterSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 | import { AppThunk, RootState } from "../../app/store";
3 |
4 | interface CounterState {
5 | value: number;
6 | }
7 |
8 | const initialState: CounterState = {
9 | value: 0,
10 | };
11 |
12 | export const counterSlice = createSlice({
13 | name: "counter",
14 | initialState,
15 | reducers: {
16 | increment: (state) => {
17 | state.value += 1;
18 | },
19 | decrement: (state) => {
20 | state.value -= 1;
21 | },
22 | incrementByAmount: (state, action: PayloadAction) => {
23 | state.value += action.payload;
24 | },
25 | },
26 | });
27 |
28 | // export actions
29 | export const { increment, decrement, incrementByAmount } = counterSlice.actions;
30 |
31 | // example for thunk action
32 | export const incrementAsync = (amount: number): AppThunk => (dispatch) => {
33 | setTimeout(() => {
34 | dispatch(incrementByAmount(amount));
35 | }, 1000);
36 | };
37 |
38 | // selector for count
39 | export const selectCount = (state: RootState) => state.counter.value;
40 |
41 | // export reducer
42 | export default counterSlice.reducer;
43 |
--------------------------------------------------------------------------------
/src/features/widgets/Lumino.css:
--------------------------------------------------------------------------------
1 | @import "../../../node_modules/@lumino/dragdrop/style/index.css";
2 | @import "../../../node_modules/@lumino/widgets/style/index.css";
3 | @import "../../../node_modules/@lumino/default-theme/style/commandpalette.css";
4 | @import "../../../node_modules/@lumino/default-theme/style/datagrid.css";
5 | @import "../../../node_modules/@lumino/default-theme/style/dockpanel.css";
6 | @import "../../../node_modules/@lumino/default-theme/style/menu.css";
7 | @import "../../../node_modules/@lumino/default-theme/style/menubar.css";
8 | @import "../../../node_modules/@lumino/default-theme/style/scrollbar.css";
9 | @import "../../../node_modules/@lumino/default-theme/style/tabbar.css";
10 |
11 | #menuBar {
12 | flex: 0 0 auto;
13 | }
14 |
15 | #main {
16 | flex: 1 1 auto;
17 | height: calc(100vh - 48px);
18 | }
19 |
20 | #palette {
21 | min-width: 300px;
22 | border-right: 1px solid #dddddd;
23 | }
24 |
25 | .content {
26 | position: relative;
27 | min-width: 50px;
28 | min-height: 50px;
29 | display: flex;
30 | flex-direction: column;
31 | padding: 8px;
32 | border: 1px solid #c0c0c0;
33 | border-top: none;
34 | background: white;
35 | }
36 |
37 | .p-DockPanel-handle,
38 | .lm-DockPanel-handle {
39 | width: 1px;
40 | }
41 |
42 | .p-TabBar-content,
43 | .lm-TabBar-content {
44 | border: 1px solid #c0c0c0;
45 | background: rgba(243, 244, 246);
46 | }
47 | .p-TabBar-tab,
48 | .lm-TabBar-tab {
49 | background: rgba(243, 244, 246);
50 | min-height: 22px;
51 | max-height: 22px;
52 | }
53 |
54 | .p-TabBar-tab.p-mod-current,
55 | .lm-tabBar-tab.lm-mod-current {
56 | min-height: 24px;
57 | max-height: 24px;
58 | transform: translate(-1px, 1px);
59 | background: white;
60 | }
61 |
62 | .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon:before,
63 | .lm-TabBar-tab.lm-mod-closable > .lm-TabBar-tabCloseIcon:before {
64 | border-radius: 100%;
65 | width: 1rem;
66 | height: 1rem;
67 | content: "X";
68 | cursor: pointer;
69 | }
70 |
71 | .p-DockPanel-overlay,
72 | .lm-DockPanel-overlay {
73 | border: none;
74 | background: rgba(132, 196, 255, 0.6);
75 | transition-property: top, left, right, bottom;
76 | transition-duration: 150ms;
77 | transition-timing-function: ease;
78 | }
79 |
--------------------------------------------------------------------------------
/src/features/widgets/Lumino.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom";
2 | import React, { useEffect, useRef, useState, useCallback } from "react";
3 | import { BoxPanel, DockPanel, Widget } from "@lumino/widgets";
4 | import { Provider, useSelector } from "react-redux";
5 | import { store, useAppDispatch } from "../../app/store";
6 | import {
7 | selectWidgets,
8 | AppWidget,
9 | AppWidgetType,
10 | deleteWidget,
11 | activateWidget,
12 | } from "./widgetsSlice";
13 | import Watcher from "../counter/Watcher";
14 | import Incrementor from "../counter/Incrementor";
15 | import Decrementor from "../counter/Decrementor";
16 | import "./Lumino.css";
17 |
18 | /**
19 | * LuminoWidget allows us to fire custom events to the HTMLElement that is holding all
20 | * the widgets. This approach handles the plumbing between Lumino and React/Redux
21 | */
22 | class LuminoWidget extends Widget {
23 | name: string; // will be displayed in the tab
24 | closable: boolean; // make disable closing on some widgets if you want
25 | mainRef: HTMLDivElement; // reference to the element holding the widgets to fire events
26 | constructor(
27 | id: string,
28 | name: string,
29 | mainRef: HTMLDivElement,
30 | closable = true
31 | ) {
32 | super({ node: LuminoWidget.createNode(id) });
33 |
34 | this.id = id;
35 | this.name = name;
36 | this.mainRef = mainRef;
37 | this.closable = closable;
38 |
39 | this.setFlag(Widget.Flag.DisallowLayout);
40 | this.addClass("content");
41 |
42 | this.title.label = name; // this sets the tab name
43 | this.title.closable = closable;
44 | }
45 |
46 | static createNode(id: string) {
47 | const div = document.createElement("div");
48 | div.setAttribute("id", id);
49 | return div;
50 | }
51 |
52 | /**
53 | * this event is triggered when we click on the tab of a widget
54 | */
55 | onActivateRequest(msg: any) {
56 | // create custom event
57 | const event = new CustomEvent("lumino:activated", this.getEventDetails());
58 | // fire custom event to parent element
59 | this.mainRef?.dispatchEvent(event);
60 | // continue with normal Widget behaviour
61 | super.onActivateRequest(msg);
62 | }
63 |
64 | /**
65 | * this event is triggered when the user clicks the close button
66 | */
67 | onCloseRequest(msg: any) {
68 | // create custom event
69 | const event = new CustomEvent("lumino:deleted", this.getEventDetails());
70 | // fire custom event to parent element
71 | this.mainRef?.dispatchEvent(event);
72 | // continue with normal Widget behaviour
73 | super.onCloseRequest(msg);
74 | }
75 |
76 | /**
77 | * creates a LuminoEvent holding name/id to properly handle them in react/redux
78 | */
79 | private getEventDetails(): LuminoEvent {
80 | return {
81 | detail: {
82 | id: this.id,
83 | name: this.name,
84 | closable: this.closable,
85 | },
86 | };
87 | }
88 | }
89 |
90 | /**
91 | * This is the type of the custom event we use to communicate from lumino to react/redux
92 | */
93 | export interface LuminoEvent {
94 | detail: { id: string; name: string; closable: boolean };
95 | }
96 |
97 | /**
98 | * Props of any component that will be rendered inside a LuminoWidget
99 | */
100 | export interface ReactWidgetProps {
101 | id: string;
102 | name: string;
103 | }
104 |
105 | /**
106 | * Type of any component that will be rendered inside a LuminoWidget
107 | */
108 | export type ReactWidget = React.FC;
109 |
110 | /**
111 | * Method to return the component corresponding to the widgettype
112 | */
113 | const getComponent = (type: AppWidgetType): ReactWidget => {
114 | switch (type) {
115 | case "WATCHER":
116 | return Watcher;
117 | case "INCREMENTOR":
118 | return Incrementor;
119 | case "DECREMENTOR":
120 | return Decrementor;
121 | default:
122 | return Watcher;
123 | }
124 | };
125 |
126 | /**
127 | * Initialize Boxpanel and Dockpanel globally once to handle future calls
128 | */
129 | const main = new BoxPanel({ direction: "left-to-right", spacing: 0 });
130 | const dock = new DockPanel();
131 |
132 | /**
133 | * This component watches the widgets redux state and draws them
134 | */
135 | const Lumino: React.FC = () => {
136 | const [attached, setAttached] = useState(false); // avoid attaching DockPanel and BoxPanel twice
137 | const mainRef = useRef(null); // reference for Element holding our Widgets
138 | const [renderedWidgetIds, setRenderedWidgetIds] = useState([]); // tracker of components that have been rendered with LuminoWidget already
139 | const widgets = useSelector(selectWidgets); // widgetsState
140 | const dispatch = useAppDispatch();
141 |
142 | /**
143 | * creates a LuminoWidget and adds it to the DockPanel. Id of widget is added to renderedWidgets
144 | */
145 | const addWidget = useCallback((w: AppWidget) => {
146 | if (mainRef.current === null) return;
147 | setRenderedWidgetIds((cur) => [...cur, w.id]);
148 | const lum = new LuminoWidget(w.id, w.tabTitle, mainRef.current, true);
149 | dock.addWidget(lum);
150 | }, []);
151 |
152 | /**
153 | * watch widgets state and calls addWidget for Each. After addWidget is executed we look
154 | * for the element in the DOM and use React to render the Component into the widget
155 | * NOTE: We need to use Provider in order to access the Redux State inside the widgets.
156 | */
157 | useEffect(() => {
158 | if (!attached) return;
159 | widgets.forEach((w) => {
160 | if (renderedWidgetIds.includes(w.id)) return; // avoid drawing widgets twice
161 | addWidget(w); // addWidget to DOM
162 | const el = document.getElementById(w.id); // get DIV
163 | const Component = getComponent(w.type); // get Component for TYPE
164 | if (el) {
165 | ReactDOM.render(
166 | // draw Component into Lumino DIV
167 |
168 |
169 | ,
170 | el
171 | );
172 | }
173 | });
174 | }, [widgets, attached, addWidget, renderedWidgetIds]);
175 |
176 | /**
177 | * This effect initializes the BoxPanel and the Dockpanel and adds event listeners
178 | * to dispatch proper Redux Actions for our custom events
179 | */
180 | useEffect(() => {
181 | if (mainRef.current === null || attached === true) {
182 | return;
183 | }
184 | main.id = "main";
185 | main.addClass("main");
186 | dock.id = "dock";
187 | window.onresize = () => main.update();
188 | BoxPanel.setStretch(dock, 1);
189 | Widget.attach(main, mainRef.current);
190 | setAttached(true);
191 | main.addWidget(dock);
192 | // dispatch activated action
193 | mainRef.current.addEventListener("lumino:activated", (e: Event) => {
194 | const le = (e as unknown) as LuminoEvent;
195 | dispatch(activateWidget(le.detail.id));
196 | });
197 | // dispatch deleted action
198 | mainRef.current.addEventListener("lumino:deleted", (e: Event) => {
199 | const le = (e as unknown) as LuminoEvent;
200 | dispatch(deleteWidget(le.detail.id));
201 | });
202 | }, [mainRef, attached, dispatch]);
203 |
204 | return
;
205 | };
206 |
207 | export default Lumino;
208 |
--------------------------------------------------------------------------------
/src/features/widgets/widgetsSlice.ts:
--------------------------------------------------------------------------------
1 | import { nanoid, PayloadAction, createSlice } from "@reduxjs/toolkit";
2 | import { RootState } from "../../app/store";
3 |
4 | /**
5 | * Types of Widgets for this counter example
6 | */
7 | export type AppWidgetType = "INCREMENTOR" | "DECREMENTOR" | "WATCHER";
8 |
9 | export interface AppWidget {
10 | type: AppWidgetType;
11 | id: string;
12 | tabTitle: string;
13 | active: boolean;
14 | }
15 | /**
16 | * State that holds the widget information
17 | */
18 | export interface WidgetsState {
19 | widgets: AppWidget[];
20 | }
21 |
22 | /**
23 | * Draw one Watcher initially
24 | */
25 | const initialState: WidgetsState = {
26 | widgets: [
27 | { type: "WATCHER", id: nanoid(), tabTitle: "Watcher", active: true },
28 | ],
29 | };
30 |
31 | /**
32 | * create a slice for handling basic widget actions: add, delete, activate
33 | */
34 | export const widgetsSlice = createSlice({
35 | name: "widgets",
36 | initialState,
37 | reducers: {
38 | addWidget: (state, action: PayloadAction) => {
39 | state.widgets.push(action.payload);
40 | },
41 | deleteWidget: (state, action: PayloadAction) => {
42 | state.widgets = state.widgets.filter((w) => w.id !== action.payload);
43 | },
44 | activateWidget: (state, action: PayloadAction) => {
45 | state.widgets = state.widgets.map((w) => {
46 | w.active = w.id === action.payload;
47 | return w;
48 | });
49 | },
50 | },
51 | });
52 |
53 | // export actions
54 | export const { addWidget, deleteWidget, activateWidget } = widgetsSlice.actions;
55 |
56 | /**
57 | * shorthand for adding an incrementor
58 | */
59 | export const addIncrementor = () =>
60 | addWidget({
61 | id: nanoid(),
62 | active: true,
63 | tabTitle: "Incrementor",
64 | type: "INCREMENTOR",
65 | });
66 |
67 | /**
68 | * shorthand for adding a decrementor
69 | */
70 | export const addDecrementor = () =>
71 | addWidget({
72 | id: nanoid(),
73 | active: true,
74 | tabTitle: "Decrementor",
75 | type: "DECREMENTOR",
76 | });
77 |
78 | /**
79 | * shorthand for adding a watcher
80 | */
81 | export const addWatcher = () =>
82 | addWidget({
83 | id: nanoid(),
84 | active: true,
85 | tabTitle: "Watcher",
86 | type: "WATCHER",
87 | });
88 |
89 | /**
90 | * selector for the widgets
91 | */
92 | export const selectWidgets = (state: RootState) => state.widgets.widgets;
93 |
94 | export default widgetsSlice.reducer;
95 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | flex-direction: column;
4 | position: absolute;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | margin: 0;
10 | padding: 0;
11 | overflow: hidden;
12 | }
13 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import { store } from "./app/store";
5 | import { Provider } from "react-redux";
6 |
7 | import "./index.css";
8 |
9 | ReactDOM.render(
10 |
11 |
12 |
13 |
14 | ,
15 | document.getElementById("root")
16 | );
17 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------