├── .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 | ![demo animation](./demo.gif) 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 | 19 | 22 | 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 | 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 | 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 | --------------------------------------------------------------------------------