├── .eslintrc ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets ├── icons │ ├── LICENSE │ ├── chrome.png │ ├── electron.png │ ├── erwt.png │ ├── license.png │ ├── nodejs.png │ ├── react.png │ ├── typescript.png │ └── webpack.png └── images │ ├── appIcon.ico │ ├── app_screen.png │ └── logo.png ├── misc └── window │ ├── components │ ├── ControlButton.tsx │ ├── Titlebar.less │ ├── Titlebar.tsx │ ├── WindowControls.tsx │ └── WindowFrame.tsx │ ├── titlebarContext.ts │ ├── titlebarContextApi.ts │ ├── titlebarIPC.ts │ ├── titlebarMenus.ts │ └── windowPreload.ts ├── package.json ├── src ├── common │ └── helpers.ts ├── main │ ├── app.ts │ └── appWindow.ts ├── renderer │ ├── app.html │ ├── appPreload.tsx │ ├── appRenderer.tsx │ └── components │ │ ├── Application.scss │ │ ├── Application.tsx │ │ ├── Icons.tsx │ │ └── Theme.scss └── types │ └── index.d.ts ├── tools ├── forge │ └── forge.config.js └── webpack │ ├── webpack.aliases.js │ ├── webpack.helpers.js │ ├── webpack.main.js │ ├── webpack.plugins.js │ ├── webpack.renderer.js │ └── webpack.rules.js ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:import/errors", 13 | "plugin:import/warnings" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "settings": { 17 | "import/resolver": { 18 | "node": { 19 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 20 | }, 21 | "alias": { 22 | "map": [ 23 | ["@renderer", "./src/renderer"], 24 | ["@components", "./src/renderer/components"], 25 | ["@common", "./src/common"], 26 | ["@main", "./src/main"], 27 | ["@src", "./src"], 28 | ["@misc", "./misc"], 29 | ["@assets", "./assets"] 30 | ], 31 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 32 | } 33 | }, 34 | "react": { 35 | "version": "latest" 36 | } 37 | }, 38 | "rules": { 39 | "react/prop-types": "off", 40 | "@typescript-eslint/no-var-requires": "off" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript cache 40 | *.tsbuildinfo 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | 82 | # Webpack 83 | .webpack/ 84 | 85 | # Electron-Forge 86 | out/ 87 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "jsxSingleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Darkhorse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron React Webpack Typescript Application 2 | 3 | A minimal secure boilerplate for writing Desktop Applications using [Electron](https://www.electronjs.org/), [React](https://reactjs.org/), [Webpack](https://webpack.js.org/) & [TypeScript](https://www.typescriptlang.org/) with Custom Titlebar. 4 | 5 | 6 |
7 | 8 | 9 |
10 | 11 | # Custom Electron Window Titlebar & Menus etc. 12 | 13 | **Following are the list of features it provides :** 14 | 15 | - Custom Titlebar for Electron Window. 16 | - Easily changable platform specific controls for max/min/close buttons using `windows` or `mac` value for `platform` property with `` in renderer. 17 | - Titlebar menus can show/hide by pressing `alt` or `option` key. 18 | - Window frame `title` prop displays in titlebar center when menus are toggeled off. 19 | - Menu entries can be customized in `misc/window/titlebarMenus.ts` file. 20 | - Menu items and windows controls layout or colors can be customized easily by modifying the `misc/window` modules. 21 | 22 |

23 | 24 | # Core Features 25 | 26 | - 🌟 Electron 27 | - 🌀 TypeScript 28 | - ⚛️ React 29 | - 🥗 SASS/SCSS Loader 30 | - 🛶 LESS Loader (optional) 31 | - 🎨 CSS Loader 32 | - 📸 Image Loader 33 | - 🆎 Font Loader 34 | - 🧹 ESLint 35 | - 📦 Electron Forge 36 | - 📐 Custom Window Frame 37 | - 📐 Custom Window Titlebar 38 | - 📐 Custom Window Menubar 39 | - 🔱 Webpack & Configuration 40 | - 🧩 Aliases for Project Paths 41 | - 🔥 React Fast Refresh + Webpack HMR 42 | - 🌞 Dark Mode + Light Mode (Theme) 43 | - 🎁 Package Bundling (Distribution / Release) 44 | 45 |
46 | 47 | ## Custom Aliases for Paths 48 | 49 | We can use predefined aliases for `import` paths already used in this project. Following are the details: 50 | 51 | | Alias | Target Path | 52 | | ------------- | -------------------------- | 53 | | `@assets` | `/assets` | 54 | | `@main` | `/src/main` | 55 | | `@renderer` | `/src/renderer` | 56 | | `@common` | `/src/common` | 57 | | `@misc` | `/misc` | 58 | | `@src` | `/src` | 59 | | `@components` | `/src/renderer/components` | 60 | 61 |

62 | 63 | # Installation 64 | 65 |
66 | 67 | Install dependencies using [pnpm](https://pnpm.io/) or [yarn](https://www.npmjs.com/package/yarn) or [npm](https://www.npmjs.com/) : 68 | 69 | ```bash 70 | # using pnpm 71 | pnpm install 72 | 73 | # or using yarn 74 | yarn install 75 | 76 | # or using npm 77 | npm install 78 | ``` 79 | 80 |
81 | 82 | ## Start : Development 83 | 84 | To develop and run your application, you need to run following command. 85 |
86 | Start electron application for development : 87 | 88 | ```bash 89 | yarn start 90 | ``` 91 | 92 |
93 | 94 | ## Lint : Development 95 | 96 | To lint application source code using ESLint via this command : 97 | 98 | ```bash 99 | yarn lint 100 | ``` 101 | 102 |
103 | 104 | ## Package : Production 105 | 106 | Customize and package your Electron app with OS-specific bundles (.app, .exe etc) 107 | 108 | ```bash 109 | yarn package 110 | ``` 111 | 112 |
113 | 114 | ## Make : Production 115 | 116 | Making is a way of taking your packaged application and making platform specific distributables like DMG, EXE, or Flatpak files (amongst others). 117 | 118 | ```bash 119 | yarn make 120 | ``` 121 | 122 |
123 | 124 | ## Publish : Production 125 | 126 | Publishing is a way of taking the artifacts generated by the `make` command and sending them to a service somewhere for you to distribute or use as updates. (This could be your update server or an S3 bucket) 127 | 128 | ```bash 129 | yarn publish 130 | ``` 131 | 132 |
133 | 134 | ## Packager & Makers Configuration 135 | 136 | This provides an easy way of configuring your packaged application and making platform specific distributables like DMG, EXE, or Flatpak files. 137 | 138 | This configurations file is available in : 139 | 140 | ```bash 141 | tools/forge/forge.config.js 142 | ``` 143 | 144 | For further information, you can visit [Electron Forge Configuration](https://www.electronforge.io/configuration) 145 | -------------------------------------------------------------------------------- /assets/icons/LICENSE: -------------------------------------------------------------------------------- 1 | # Attribution 2 | 3 | Thanks to Flaticon -------------------------------------------------------------------------------- /assets/icons/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/icons/chrome.png -------------------------------------------------------------------------------- /assets/icons/electron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/icons/electron.png -------------------------------------------------------------------------------- /assets/icons/erwt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/icons/erwt.png -------------------------------------------------------------------------------- /assets/icons/license.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/icons/license.png -------------------------------------------------------------------------------- /assets/icons/nodejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/icons/nodejs.png -------------------------------------------------------------------------------- /assets/icons/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/icons/react.png -------------------------------------------------------------------------------- /assets/icons/typescript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/icons/typescript.png -------------------------------------------------------------------------------- /assets/icons/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/icons/webpack.png -------------------------------------------------------------------------------- /assets/images/appIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/images/appIcon.ico -------------------------------------------------------------------------------- /assets/images/app_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/images/app_screen.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sailingdev/electron-react-typescript/9f75ed03b5df08c5b124a86791cc79034d4f6404/assets/images/logo.png -------------------------------------------------------------------------------- /misc/window/components/ControlButton.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | 4 | interface IControlButtonProps { 5 | readonly name: string; 6 | readonly path: string; 7 | readonly title: string; 8 | } 9 | 10 | const ControlButton: React.FC< 11 | IControlButtonProps & React.HTMLAttributes 12 | > = (props) => { 13 | const { name, path, title, ...rest } = props; 14 | const { onClick } = rest; 15 | 16 | const className = classNames('control', name); 17 | 18 | return ( 19 |
26 | 29 |
30 | ); 31 | }; 32 | 33 | export default ControlButton; 34 | -------------------------------------------------------------------------------- /misc/window/components/Titlebar.less: -------------------------------------------------------------------------------- 1 | @titlebar-baseSize: 16px; 2 | @titlebar-height: 28px; 3 | @titlebar-bg: #171b21; 4 | @titlebar-iconSize: 16px; 5 | @em: @titlebar-baseSize*1em; 6 | 7 | //----------------------------------------- 8 | // Mixins 9 | //----------------------------------------- 10 | 11 | .flex-strech() { 12 | display: flex; 13 | align-items: stretch; 14 | } 15 | 16 | .flex-align-center() { 17 | display: flex; 18 | align-items: center; 19 | } 20 | 21 | //----------------------------------------- 22 | // Stylesheet 23 | //----------------------------------------- 24 | 25 | .window-content { 26 | position: relative; 27 | overflow: auto; 28 | flex: 1; 29 | } 30 | 31 | .window-titlebar { 32 | .flex-strech(); 33 | font-size: @titlebar-baseSize; 34 | height: @titlebar-height; 35 | background: @titlebar-bg; 36 | -webkit-app-region: drag; // Draggable 37 | user-select: none; 38 | position: relative; 39 | 40 | &>section { 41 | .flex-align-center(); 42 | } 43 | 44 | &-content { 45 | flex: 1; 46 | font-size: calc(@titlebar-baseSize - 3px); 47 | color: #a9b0bb; 48 | 49 | &.centered { 50 | width: 100%; 51 | height: 100%; 52 | position: absolute; 53 | justify-content: center; 54 | } 55 | } 56 | 57 | // Titlebar icon 58 | &-icon { 59 | padding: 0 0.75em; 60 | 61 | img { 62 | width: @titlebar-iconSize; 63 | height: @titlebar-iconSize; 64 | } 65 | } 66 | 67 | // Titlebar Menu 68 | &-menu { 69 | flex: 1; 70 | } 71 | 72 | .menu { 73 | &-item { 74 | position: relative; 75 | } 76 | 77 | &-item.active { 78 | .menu-title { 79 | background: #3c4043; 80 | color: #bfbfbf; 81 | } 82 | } 83 | 84 | &-title { 85 | padding: 4px 10px; 86 | font-size: 0.8125rem; 87 | text-shadow: 0px 1px 1px black; 88 | -webkit-app-region: no-drag; 89 | color: #97a0b1; 90 | 91 | &:hover { 92 | background-color: #1f252c; 93 | } 94 | } 95 | 96 | &-popup { 97 | display: none; 98 | position: fixed; 99 | background: #292a2d; 100 | min-width: 70px; 101 | border: 1px solid #3c4043; 102 | padding: 0.25rem 0; 103 | box-shadow: 2px 1px 4px hsla(0, 0%, 0%, 0.5); 104 | z-index: 10000; 105 | 106 | &.active { 107 | display: block; 108 | } 109 | 110 | &-item { 111 | display: flex; 112 | justify-content: space-between; 113 | font-size: 0.8125rem; 114 | text-shadow: 0px 1px 1px black; 115 | padding: 0.375rem 1rem; 116 | 117 | &:hover { 118 | background: #1761cb; 119 | 120 | .popup-item-shortcut { 121 | color: #8cbbff; 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | .popup-item { 129 | &-name { 130 | padding-right: 2rem; 131 | color: #d8d8d8; 132 | } 133 | 134 | &-shortcut { 135 | color: #73757c; 136 | text-shadow: none; 137 | } 138 | 139 | &-separator { 140 | height: 1px; 141 | background: #3c4043; 142 | margin: 4px 0; 143 | } 144 | } 145 | 146 | // Titlebar controls 147 | &-controls { 148 | .flex-strech(); 149 | position: absolute; 150 | right: 0; 151 | top: 0; 152 | bottom: 0; 153 | color: #969799; 154 | 155 | &.type-windows { 156 | .control { 157 | padding: 0 1.15em; 158 | font-size: 0.875em; 159 | display: flex; 160 | height: 100%; 161 | align-items: center; 162 | -webkit-app-region: no-drag; 163 | 164 | &.close:hover { 165 | background: #e10000; 166 | } 167 | 168 | &:hover { 169 | background: #242d38; 170 | color: #d8d9db; 171 | } 172 | } 173 | } 174 | 175 | &.type-mac { 176 | .control { 177 | width: 16px; 178 | height: 16px; 179 | background: #0e0e0e99; 180 | border-radius: 50%; 181 | display: flex; 182 | align-items: center; 183 | justify-content: center; 184 | margin-right: 0.675rem; 185 | color: transparent; 186 | -webkit-app-region: no-drag; 187 | opacity: 0.8; 188 | 189 | &:hover { 190 | opacity: 1; 191 | } 192 | } 193 | 194 | .control.close { 195 | background: #f46d60; 196 | } 197 | 198 | .control.maximize { 199 | background: #59ca56; 200 | } 201 | 202 | .control.minimize { 203 | background: #f9c04e; 204 | } 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /misc/window/components/Titlebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef, useContext, useEffect, useRef, useState } from 'react'; 2 | import titlebarMenus from '../titlebarMenus'; 3 | import classNames from 'classnames'; 4 | import WindowControls from './WindowControls'; 5 | import context from '../titlebarContextApi'; 6 | import { WindowContext } from './WindowFrame'; 7 | import './Titlebar.less'; 8 | 9 | type Props = { 10 | title: string; 11 | mode: 'centered-title'; 12 | icon?: string; 13 | }; 14 | 15 | const Titlebar: React.FC = (props) => { 16 | const activeMenuIndex = useRef(null); 17 | const menusRef = titlebarMenus.map(() => createRef()); 18 | const [menusVisible, setMenusVisible] = useState(true); 19 | const windowContext = useContext(WindowContext); 20 | 21 | useEffect(() => { 22 | const handleKeyDown = (e: KeyboardEvent) => { 23 | if (e.repeat) return; // Prevent repeatation of toggle when key holding 24 | if (e.altKey) { 25 | // Hiding menus? close active menu popup 26 | if (menusVisible) { 27 | closeActiveMenu(); 28 | } 29 | setMenusVisible(!menusVisible); 30 | } 31 | }; 32 | 33 | document.addEventListener('keydown', handleKeyDown); 34 | 35 | return () => { 36 | document.removeEventListener('keydown', handleKeyDown); 37 | }; 38 | }, [menusVisible, menusRef]); 39 | 40 | useEffect(() => { 41 | function handleClickOutside(event: MouseEvent) { 42 | if (activeMenuIndex.current != null) { 43 | if ( 44 | menusRef[activeMenuIndex.current].current && 45 | !menusRef[activeMenuIndex.current].current?.contains(event.target as Node) 46 | ) { 47 | // console.log('You clicked outside of me!'); 48 | closeActiveMenu(); 49 | } 50 | } 51 | } 52 | 53 | if (activeMenuIndex != null) { 54 | document.addEventListener('mousedown', handleClickOutside); 55 | // console.log('added event'); 56 | } 57 | 58 | return () => { 59 | document.removeEventListener('mousedown', handleClickOutside); 60 | // console.log('remove event'); 61 | }; 62 | }, [activeMenuIndex, menusRef]); 63 | 64 | function showMenu(index: number, e: React.MouseEvent) { 65 | e.stopPropagation(); 66 | e.preventDefault(); 67 | 68 | if (menusRef[index].current?.classList.contains('active')) { 69 | // close.. 70 | closeActiveMenu(); 71 | } else { 72 | // open.. 73 | menusRef[index].current?.classList.add('active'); 74 | activeMenuIndex.current = index; 75 | menusRef[index].current?.parentElement?.classList.add('active'); 76 | } 77 | } 78 | 79 | function onMenuHover(index: number) { 80 | if (activeMenuIndex.current != null) { 81 | menusRef[activeMenuIndex.current].current?.classList.toggle('active'); 82 | menusRef[index].current?.classList.toggle('active'); 83 | menusRef[index].current?.parentElement?.classList.toggle('active'); 84 | menusRef[activeMenuIndex.current].current?.parentElement?.classList.toggle( 85 | 'active', 86 | ); 87 | 88 | activeMenuIndex.current = index; 89 | } 90 | } 91 | 92 | function closeActiveMenu() { 93 | if (activeMenuIndex.current != null) { 94 | menusRef[activeMenuIndex.current].current?.classList.remove('active'); 95 | menusRef[activeMenuIndex.current]?.current?.parentElement?.classList.remove('active'); 96 | activeMenuIndex.current = null; 97 | } 98 | } 99 | 100 | function handleAction(action?: string, value?: string | number) { 101 | closeActiveMenu(); 102 | const c: Record = context; 103 | if (action) { 104 | if (typeof c[action] === 'function') { 105 | c[action](value); 106 | } else { 107 | console.log(`action [${action}] is not available in titlebar context`); 108 | } 109 | } 110 | } 111 | 112 | return ( 113 |
114 | {props.icon ? ( 115 |
116 | titlebar icon 117 |
118 | ) : ( 119 | '' 120 | )} 121 | 122 |
127 | {menusVisible ? '' :
{props.title}
} 128 |
129 | 130 |
135 | {titlebarMenus.map((item, menuIndex) => { 136 | return ( 137 |
138 |
showMenu(menuIndex, e)} 141 | onMouseEnter={() => onMenuHover(menuIndex)} 142 | onMouseDown={(e) => e.preventDefault()} 143 | > 144 | {item.name} 145 |
146 |
147 | {item.items?.map((menuItem, menuItemIndex) => { 148 | if (menuItem.name === '__') { 149 | return ( 150 |
154 | ); 155 | } 156 | 157 | return ( 158 |
162 | handleAction(menuItem.action, menuItem.value) 163 | } 164 | onMouseDown={(e) => e.preventDefault()} 165 | > 166 |
{menuItem.name}
167 |
168 | {menuItem.shortcut} 169 |
170 |
171 | ); 172 | })} 173 |
174 |
175 | ); 176 | })} 177 |
178 | 179 | 180 |
181 | ); 182 | }; 183 | 184 | export default Titlebar; 185 | -------------------------------------------------------------------------------- /misc/window/components/WindowControls.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | import context from '../titlebarContextApi'; 4 | 5 | import ControlButton from './ControlButton'; 6 | 7 | type Props = { 8 | platform: string; 9 | tooltips?: boolean; 10 | }; 11 | 12 | const closePath = 13 | 'M 0,0 0,0.7 4.3,5 0,9.3 0,10 0.7,10 5,5.7 9.3,10 10,10 10,9.3 5.7,5 10,0.7 10,0 9.3,0 5,4.3 0.7,0 Z'; 14 | const maximizePath = 'M 0,0 0,10 10,10 10,0 Z M 1,1 9,1 9,9 1,9 Z'; 15 | const minimizePath = 'M 0,5 10,5 10,6 0,6 Z'; 16 | 17 | const WindowControls: React.FC = (props) => { 18 | return ( 19 |
25 | context.minimize()} 28 | path={minimizePath} 29 | title={props.tooltips ? 'Minimize' : null} 30 | /> 31 | context.toggle_maximize()} 34 | path={maximizePath} 35 | title={props.tooltips ? 'Maximize' : null} 36 | /> 37 | context.exit()} 40 | path={closePath} 41 | title={props.tooltips ? 'Close' : null} 42 | /> 43 |
44 | ); 45 | }; 46 | 47 | export default WindowControls; 48 | -------------------------------------------------------------------------------- /misc/window/components/WindowFrame.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import Titlebar from './Titlebar'; 3 | import logo from '@assets/images/logo.png'; 4 | 5 | type Props = { 6 | title?: string; 7 | borderColor?: string; 8 | platform: 'windows' | 'mac'; 9 | children: React.ReactNode; 10 | }; 11 | 12 | type Context = { 13 | platform: 'windows' | 'mac'; 14 | }; 15 | 16 | export const WindowContext = React.createContext({ 17 | platform: 'windows', 18 | }); 19 | 20 | const WindowFrame: React.FC = (props) => { 21 | const itsRef = useRef(null); 22 | 23 | useEffect(() => { 24 | const { parentElement } = itsRef.current; 25 | parentElement.classList.add('has-electron-window'); 26 | parentElement.classList.add('has-border'); 27 | 28 | // Apply border color if prop given 29 | if (props.borderColor) { 30 | parentElement.style.borderColor = props.borderColor; 31 | } 32 | }, []); 33 | 34 | return ( 35 | 36 | {/* Reference creator */} 37 |
38 | {/* Window Titlebar */} 39 | 44 | {/* Window Content (Application to render) */} 45 |
{props.children}
46 |
47 | ); 48 | }; 49 | 50 | export default WindowFrame; 51 | -------------------------------------------------------------------------------- /misc/window/titlebarContext.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | const titlebarContext = { 4 | exit() { 5 | ipcRenderer.invoke('window-close'); 6 | }, 7 | undo() { 8 | ipcRenderer.invoke('web-undo'); 9 | }, 10 | redo() { 11 | ipcRenderer.invoke('web-redo'); 12 | }, 13 | cut() { 14 | ipcRenderer.invoke('web-cut'); 15 | }, 16 | copy() { 17 | ipcRenderer.invoke('web-copy'); 18 | }, 19 | paste() { 20 | ipcRenderer.invoke('web-paste'); 21 | }, 22 | delete() { 23 | ipcRenderer.invoke('web-delete'); 24 | }, 25 | select_all() { 26 | ipcRenderer.invoke('web-select-all'); 27 | }, 28 | reload() { 29 | ipcRenderer.invoke('web-reload'); 30 | }, 31 | force_reload() { 32 | ipcRenderer.invoke('web-force-reload'); 33 | }, 34 | toggle_devtools() { 35 | ipcRenderer.invoke('web-toggle-devtools'); 36 | }, 37 | actual_size() { 38 | ipcRenderer.invoke('web-actual-size'); 39 | }, 40 | zoom_in() { 41 | ipcRenderer.invoke('web-zoom-in'); 42 | }, 43 | zoom_out() { 44 | ipcRenderer.invoke('web-zoom-out'); 45 | }, 46 | toggle_fullscreen() { 47 | ipcRenderer.invoke('web-toggle-fullscreen'); 48 | }, 49 | minimize() { 50 | ipcRenderer.invoke('window-minimize'); 51 | }, 52 | toggle_maximize() { 53 | ipcRenderer.invoke('window-toggle-maximize'); 54 | }, 55 | open_url(url: string) { 56 | ipcRenderer.invoke('open-url', url); 57 | }, 58 | }; 59 | 60 | export type TitlebarContextApi = typeof titlebarContext; 61 | 62 | export default titlebarContext; 63 | -------------------------------------------------------------------------------- /misc/window/titlebarContextApi.ts: -------------------------------------------------------------------------------- 1 | import { TitlebarContextApi } from './titlebarContext'; 2 | 3 | const context: TitlebarContextApi = (window as any).electron_window?.titlebar; 4 | 5 | export default context; 6 | -------------------------------------------------------------------------------- /misc/window/titlebarIPC.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain, shell } from 'electron'; 2 | 3 | export const registerTitlebarIpc = (mainWindow: BrowserWindow) => { 4 | ipcMain.handle('window-minimize', () => { 5 | mainWindow.minimize(); 6 | }); 7 | 8 | ipcMain.handle('window-maximize', () => { 9 | mainWindow.maximize(); 10 | }); 11 | 12 | ipcMain.handle('window-toggle-maximize', () => { 13 | if (mainWindow.isMaximized()) { 14 | mainWindow.unmaximize(); 15 | } else { 16 | mainWindow.maximize(); 17 | } 18 | }); 19 | 20 | ipcMain.handle('window-close', () => { 21 | mainWindow.close(); 22 | }); 23 | 24 | ipcMain.handle('web-undo', () => { 25 | mainWindow.webContents.undo(); 26 | }); 27 | 28 | ipcMain.handle('web-redo', () => { 29 | mainWindow.webContents.redo(); 30 | }); 31 | 32 | ipcMain.handle('web-cut', () => { 33 | mainWindow.webContents.cut(); 34 | }); 35 | 36 | ipcMain.handle('web-copy', () => { 37 | mainWindow.webContents.copy(); 38 | }); 39 | 40 | ipcMain.handle('web-paste', () => { 41 | mainWindow.webContents.paste(); 42 | }); 43 | 44 | ipcMain.handle('web-delete', () => { 45 | mainWindow.webContents.delete(); 46 | }); 47 | 48 | ipcMain.handle('web-select-all', () => { 49 | mainWindow.webContents.selectAll(); 50 | }); 51 | 52 | ipcMain.handle('web-reload', () => { 53 | mainWindow.webContents.reload(); 54 | }); 55 | 56 | ipcMain.handle('web-force-reload', () => { 57 | mainWindow.webContents.reloadIgnoringCache(); 58 | }); 59 | 60 | ipcMain.handle('web-toggle-devtools', () => { 61 | mainWindow.webContents.toggleDevTools(); 62 | }); 63 | 64 | ipcMain.handle('web-actual-size', () => { 65 | mainWindow.webContents.setZoomLevel(0); 66 | }); 67 | 68 | ipcMain.handle('web-zoom-in', () => { 69 | mainWindow.webContents.setZoomLevel(mainWindow.webContents.zoomLevel + 0.5); 70 | }); 71 | 72 | ipcMain.handle('web-zoom-out', () => { 73 | mainWindow.webContents.setZoomLevel(mainWindow.webContents.zoomLevel - 0.5); 74 | }); 75 | 76 | ipcMain.handle('web-toggle-fullscreen', () => { 77 | mainWindow.setFullScreen(!mainWindow.fullScreen); 78 | }); 79 | 80 | ipcMain.handle('open-url', (e, url) => { 81 | shell.openExternal(url); 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /misc/window/titlebarMenus.ts: -------------------------------------------------------------------------------- 1 | export type TitlebarMenuItem = { 2 | name: string; 3 | action?: string; 4 | shortcut?: string; 5 | value?: string | number; 6 | items?: TitlebarMenuItem[]; 7 | }; 8 | 9 | export type TitlebarMenu = { 10 | name: string; 11 | items: TitlebarMenuItem[]; 12 | }; 13 | 14 | const titlebarMenus: TitlebarMenu[] = [ 15 | { 16 | name: 'File', 17 | items: [ 18 | { 19 | name: 'Exit', 20 | action: 'exit', 21 | }, 22 | ], 23 | }, 24 | { 25 | name: 'Edit', 26 | items: [ 27 | { 28 | name: 'Undo', 29 | action: 'undo', 30 | shortcut: 'Ctrl+Z', 31 | }, 32 | { 33 | name: 'Redo', 34 | action: 'redo', 35 | shortcut: 'Ctrl+Y', 36 | }, 37 | { 38 | name: '__', 39 | }, 40 | { 41 | name: 'Cut', 42 | action: 'cut', 43 | shortcut: 'Ctrl+X', 44 | }, 45 | { 46 | name: 'Copy', 47 | action: 'copy', 48 | shortcut: 'Ctrl+C', 49 | }, 50 | { 51 | name: 'Paste', 52 | action: 'paste', 53 | shortcut: 'Ctrl+V', 54 | }, 55 | { 56 | name: 'Delete', 57 | action: 'delete', 58 | }, 59 | { 60 | name: '__', 61 | }, 62 | { 63 | name: 'Select All', 64 | action: 'select_all', 65 | shortcut: 'Ctrl+A', 66 | }, 67 | ], 68 | }, 69 | { 70 | name: 'View', 71 | items: [ 72 | { 73 | name: 'Reload', 74 | action: 'reload', 75 | shortcut: 'Ctrl+R', 76 | }, 77 | { 78 | name: 'Force Reload', 79 | action: 'force_reload', 80 | shortcut: 'Ctrl+Shift+R', 81 | }, 82 | { 83 | name: 'Toogle Developer Tools', 84 | action: 'toggle_devtools', 85 | shortcut: 'Ctrl+Shift+I', 86 | }, 87 | { 88 | name: '__', 89 | }, 90 | { 91 | name: 'Actual Size', 92 | action: 'actual_size', 93 | shortcut: 'Ctrl+0', 94 | }, 95 | { 96 | name: 'Zoom In', 97 | action: 'zoom_in', 98 | shortcut: 'Ctrl++', 99 | }, 100 | { 101 | name: 'Zoom Out', 102 | action: 'zoom_out', 103 | shortcut: 'Ctrl+-', 104 | }, 105 | { 106 | name: '__', 107 | }, 108 | { 109 | name: 'Toggle Fullscreen', 110 | action: 'toggle_fullscreen', 111 | shortcut: 'F11', 112 | }, 113 | ], 114 | }, 115 | { 116 | name: 'Window', 117 | items: [ 118 | { 119 | name: 'Minimize', 120 | action: 'minimize', 121 | shortcut: 'Ctrl+M', 122 | }, 123 | { 124 | name: 'Close', 125 | action: 'exit', 126 | shortcut: 'Ctrl+W', 127 | }, 128 | ], 129 | }, 130 | ]; 131 | 132 | export default titlebarMenus; 133 | -------------------------------------------------------------------------------- /misc/window/windowPreload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge } from 'electron'; 2 | import titlebarContext from './titlebarContext'; 3 | 4 | contextBridge.exposeInMainWorld('electron_window', { 5 | titlebar: titlebarContext, 6 | }); 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-react-typescript-webpack-application", 3 | "version": "1.0.0", 4 | "description": "Desktop Applications using Electron, React, Webpack, TypeScript in 2023", 5 | "main": ".webpack/main", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=development electron-forge start", 8 | "package": "electron-forge package", 9 | "make": "electron-forge make", 10 | "publish": "electron-forge publish", 11 | "lint": "eslint src/ --ext .ts,.js,.tsx,.jsx" 12 | }, 13 | "keywords": [ 14 | "electron", 15 | "electron-webpack", 16 | "electron-react", 17 | "electron-typescript", 18 | "electron-react-typescript", 19 | "2023", 20 | "react", 21 | "react-typescript", 22 | "react-webpack", 23 | "create-react-app" 24 | ], 25 | "author": { 26 | "name": "Darkhorse", 27 | "url": "https://github.com/darkhorse07232020" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/darkhorse07232020/electron-react-typescript-application" 32 | }, 33 | "license": "MIT", 34 | "config": { 35 | "forge": "./tools/forge/forge.config.js" 36 | }, 37 | "devDependencies": { 38 | "@electron-forge/cli": "6.2.1", 39 | "@electron-forge/maker-deb": "6.2.1", 40 | "@electron-forge/maker-rpm": "6.2.1", 41 | "@electron-forge/maker-squirrel": "6.2.1", 42 | "@electron-forge/maker-zip": "6.2.1", 43 | "@electron-forge/plugin-webpack": "6.2.1", 44 | "@marshallofsound/webpack-asset-relocator-loader": "^0.5.0", 45 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", 46 | "@types/node": "^20.4.1", 47 | "@types/react": "^18.2.14", 48 | "@types/react-dom": "^18.2.6", 49 | "@types/webpack-env": "^1.18.1", 50 | "@typescript-eslint/eslint-plugin": "^6.0.0", 51 | "@typescript-eslint/parser": "^6.0.0", 52 | "@vercel/webpack-asset-relocator-loader": "^1.7.3", 53 | "classnames": "^2.3.2", 54 | "cross-env": "^7.0.3", 55 | "css-loader": "^6.8.1", 56 | "electron": "^25.2.0", 57 | "eslint": "^8.44.0", 58 | "eslint-import-resolver-alias": "^1.1.2", 59 | "eslint-plugin-import": "^2.27.5", 60 | "eslint-plugin-react": "^7.32.2", 61 | "file-loader": "^6.2.0", 62 | "fork-ts-checker-webpack-plugin": "^8.0.0", 63 | "less": "^4.1.3", 64 | "less-loader": "11.1.3", 65 | "node-loader": "^2.0.0", 66 | "react-refresh": "^0.14.0", 67 | "sass": "^1.63.6", 68 | "sass-loader": "^13.3.2", 69 | "style-loader": "^3.3.3", 70 | "ts-loader": "9.4.4", 71 | "typescript": "^5.1.6", 72 | "webpack": "^5.88.1" 73 | }, 74 | "dependencies": { 75 | "electron-squirrel-startup": "^1.0.0", 76 | "react": "^18.2.0", 77 | "react-dom": "^18.2.0" 78 | } 79 | } -------------------------------------------------------------------------------- /src/common/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if process NODE_ENV in 'development' mode 3 | */ 4 | export function inDev(): boolean { 5 | return process.env.NODE_ENV == 'development'; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/app.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import { createAppWindow } from './appWindow'; 3 | 4 | /** Handle creating/removing shortcuts on Windows when installing/uninstalling. */ 5 | if (require('electron-squirrel-startup')) { 6 | app.quit(); 7 | } 8 | 9 | /** 10 | * This method will be called when Electron has finished 11 | * initialization and is ready to create browser windows. 12 | * Some APIs can only be used after this event occurs. 13 | */ 14 | app.on('ready', createAppWindow); 15 | 16 | /** 17 | * Emitted when the application is activated. Various actions can 18 | * trigger this event, such as launching the application for the first time, 19 | * attempting to re-launch the application when it's already running, 20 | * or clicking on the application's dock or taskbar icon. 21 | */ 22 | app.on('activate', () => { 23 | /** 24 | * On OS X it's common to re-create a window in the app when the 25 | * dock icon is clicked and there are no other windows open. 26 | */ 27 | if (BrowserWindow.getAllWindows().length === 0) { 28 | createAppWindow(); 29 | } 30 | }); 31 | 32 | /** 33 | * Emitted when all windows have been closed. 34 | */ 35 | app.on('window-all-closed', () => { 36 | /** 37 | * On OS X it is common for applications and their menu bar 38 | * to stay active until the user quits explicitly with Cmd + Q 39 | */ 40 | if (process.platform !== 'darwin') { 41 | app.quit(); 42 | } 43 | }); 44 | 45 | /** 46 | * In this file you can include the rest of your app's specific main process code. 47 | * You can also put them in separate files and import them here. 48 | */ 49 | -------------------------------------------------------------------------------- /src/main/appWindow.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import path from 'path'; 3 | import { registerTitlebarIpc } from '@misc/window/titlebarIPC'; 4 | 5 | // Electron Forge automatically creates these entry points 6 | declare const APP_WINDOW_WEBPACK_ENTRY: string; 7 | declare const APP_WINDOW_PRELOAD_WEBPACK_ENTRY: string; 8 | 9 | let appWindow: BrowserWindow; 10 | 11 | /** 12 | * Create Application Window 13 | * @returns {BrowserWindow} Application Window Instance 14 | */ 15 | export function createAppWindow(): BrowserWindow { 16 | // Create new window instance 17 | appWindow = new BrowserWindow({ 18 | width: 800, 19 | height: 600, 20 | backgroundColor: '#202020', 21 | show: false, 22 | autoHideMenuBar: true, 23 | frame: false, 24 | titleBarStyle: 'hidden', 25 | icon: path.resolve('assets/images/appIcon.ico'), 26 | webPreferences: { 27 | nodeIntegration: false, 28 | contextIsolation: true, 29 | nodeIntegrationInWorker: false, 30 | nodeIntegrationInSubFrames: false, 31 | preload: APP_WINDOW_PRELOAD_WEBPACK_ENTRY, 32 | sandbox: false, 33 | }, 34 | }); 35 | 36 | // Load the index.html of the app window. 37 | appWindow.loadURL(APP_WINDOW_WEBPACK_ENTRY); 38 | 39 | // Show window when its ready to 40 | appWindow.on('ready-to-show', () => appWindow.show()); 41 | 42 | // Register Inter Process Communication for main process 43 | registerMainIPC(); 44 | 45 | // Close all windows when main window is closed 46 | appWindow.on('close', () => { 47 | appWindow = null; 48 | app.quit(); 49 | }); 50 | 51 | return appWindow; 52 | } 53 | 54 | /** 55 | * Register Inter Process Communication 56 | */ 57 | function registerMainIPC() { 58 | /** 59 | * Here you can assign IPC related codes for the application window 60 | * to Communicate asynchronously from the main process to renderer processes. 61 | */ 62 | registerTitlebarIpc(appWindow); 63 | } 64 | -------------------------------------------------------------------------------- /src/renderer/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Electron-React-Typescript Application 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/renderer/appPreload.tsx: -------------------------------------------------------------------------------- 1 | import '@misc/window/windowPreload'; 2 | 3 | // Say something 4 | console.log('[ERWT] : Preload execution started'); 5 | 6 | // Get versions 7 | window.addEventListener('DOMContentLoaded', () => { 8 | const app = document.getElementById('app'); 9 | const { env } = process; 10 | const versions: Record = {}; 11 | 12 | // ERWT Package version 13 | versions['erwt'] = env['npm_package_version']; 14 | versions['license'] = env['npm_package_license']; 15 | 16 | // Process versions 17 | for (const type of ['chrome', 'node', 'electron']) { 18 | versions[type] = process.versions[type].replace('+', ''); 19 | } 20 | 21 | // NPM deps versions 22 | for (const type of ['react']) { 23 | const v = env['npm_package_dependencies_' + type]; 24 | if (v) versions[type] = v.replace('^', ''); 25 | } 26 | 27 | // NPM @dev deps versions 28 | for (const type of ['webpack', 'typescript']) { 29 | const v = env['npm_package_devDependencies_' + type]; 30 | if (v) versions[type] = v.replace('^', ''); 31 | } 32 | 33 | // Set versions to app data 34 | app.setAttribute('data-versions', JSON.stringify(versions)); 35 | }); 36 | -------------------------------------------------------------------------------- /src/renderer/appRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import WindowFrame from '@misc/window/components/WindowFrame'; 4 | import Application from '@components/Application'; 5 | 6 | // Say something 7 | console.log('[ERWT] : Renderer execution started'); 8 | 9 | // Application to Render 10 | const app = ( 11 | 12 | 13 | 14 | ); 15 | 16 | // Render application in DOM 17 | createRoot(document.getElementById('app')).render(app); 18 | -------------------------------------------------------------------------------- /src/renderer/components/Application.scss: -------------------------------------------------------------------------------- 1 | @import "./Theme.scss"; 2 | 3 | /** 4 | *========================================================================== 5 | * Application Specific Stylesheet 6 | *========================================================================== 7 | * 8 | * Here we use the codes to apply application specific style 9 | */ 10 | 11 | ::selection { 12 | background: var(--selection-bgcolor); 13 | color: var(--selection-color); 14 | } 15 | 16 | ::-webkit-scrollbar { 17 | width: var(--scroll-width); 18 | 19 | &-track { 20 | background: var(--scroll-track-bgcolor); 21 | } 22 | 23 | &-thumb { 24 | background: var(--scroll-thumb-bgcolor); 25 | } 26 | 27 | &-thumb:hover { 28 | background: var(--scroll-thumb-hover-bgcolor); 29 | } 30 | } 31 | 32 | html, 33 | body, 34 | #app { 35 | height: 100%; 36 | } 37 | 38 | body { 39 | margin: 0; 40 | font-size: var(--app-font-size); 41 | font-family: var(--app-font-family); 42 | color: var(--app-color); 43 | background: var(--app-bgcolor); 44 | line-height: 1.5; 45 | } 46 | 47 | h1 { 48 | margin: 0; 49 | } 50 | 51 | #app { 52 | display: flex; 53 | flex-direction: column; 54 | box-sizing: border-box; 55 | user-select: none; 56 | 57 | &.has-border { 58 | border: var(--app-border-color); 59 | } 60 | } 61 | 62 | button { 63 | background: var(--button-bgcolor); 64 | color: var(--button-color); 65 | font-weight: normal; 66 | text-shadow: 0px 1px var(--button-shadow-color); 67 | font-family: var(--app-font-family); 68 | border: var(--button-border); 69 | padding: 0.5rem 1rem; 70 | border-radius: 6px; 71 | font-size: 0.875rem; 72 | cursor: pointer; 73 | display: inline-flex; 74 | justify-content: space-around; 75 | align-items: center; 76 | outline: none; 77 | min-width: 140px; 78 | 79 | &:hover { 80 | background: var(--button-hover-bgcolor); 81 | } 82 | &:active { 83 | background: var(--button-active-bgcolor); 84 | } 85 | & > span { 86 | color: var(--button-badge-color); 87 | background-color: var(--button-badge-bgcolor); 88 | font-size: 12px; 89 | width: 24px; 90 | height: 24px; 91 | border-radius: 50%; 92 | display: inline-flex; 93 | align-items: center; 94 | justify-content: center; 95 | display: none; 96 | } 97 | 98 | img { 99 | width: 22px; 100 | opacity: 0.8; 101 | } 102 | } 103 | 104 | .rotate { 105 | animation: rotate 4.5s linear infinite; 106 | } 107 | 108 | @keyframes rotate { 109 | to { 110 | transform: rotate(360deg); 111 | } 112 | } 113 | 114 | .main-heading { 115 | display: flex; 116 | align-items: center; 117 | justify-content: center; 118 | margin-bottom: 3rem; 119 | 120 | img { 121 | margin-right: 1rem; 122 | } 123 | 124 | h1 { 125 | font-size: 1.5rem; 126 | font-weight: 300; 127 | color: var(--erwt-heading-color); 128 | line-height: 1; 129 | text-transform: uppercase; 130 | } 131 | } 132 | 133 | .hidden { 134 | display: none !important; 135 | } 136 | 137 | .center { 138 | text-align: center; 139 | } 140 | 141 | .main-teaser { 142 | position: relative; 143 | display: flex; 144 | line-height: 25px; 145 | font-size: 14px; 146 | margin-bottom: 2em; 147 | color: gray; 148 | width: 63%; 149 | margin: 0 auto; 150 | margin-bottom: 3rem; 151 | text-align: left; 152 | padding-left: 1rem; 153 | background: #191919; 154 | padding-top: 1rem; 155 | padding-bottom: 10px; 156 | padding-right: 1rem; 157 | border-radius: 0 0 8px 8px; 158 | box-shadow: 0 8px 10px 0px #00000003; 159 | } 160 | 161 | .main-teaser:after { 162 | content: ""; 163 | position: absolute; 164 | top: 0; 165 | width: calc(100% + 40px); 166 | height: 2px; 167 | background: var(--app-accent-color); 168 | left: -20px; 169 | box-shadow: 0 10px 20px #000000e3; 170 | border-radius: 8px; 171 | } 172 | 173 | .versions { 174 | display: flex; 175 | justify-content: space-between; 176 | gap: 1rem; 177 | flex-wrap: wrap; 178 | border-radius: 10px; 179 | box-shadow: 0 0 20px inset rgb(0 0 0 / 3%); 180 | width: 80%; 181 | margin: 0 auto; 182 | } 183 | 184 | .versions .item { 185 | background: #191919; 186 | color: #d1d1d1; 187 | width: calc(50% - 1rem); 188 | padding: 4px 12px; 189 | display: flex; 190 | justify-content: space-between; 191 | font-size: 14px; 192 | margin: 0; 193 | box-sizing: border-box; 194 | border-radius: 4px; 195 | 196 | & > * { 197 | display: flex; 198 | } 199 | 200 | &-icon { 201 | width: 20px; 202 | height: 20px; 203 | margin-right: 10px; 204 | opacity: 0.8; 205 | } 206 | 207 | & > span { 208 | color: gray; 209 | text-align: right; 210 | } 211 | } 212 | 213 | #erwt { 214 | // user-select: none; 215 | display: flex; 216 | flex-direction: column; 217 | height: 100%; 218 | justify-content: space-between; 219 | background: var(--erwt-gradient); 220 | 221 | .header { 222 | padding: 3rem 2rem 0rem 2rem; 223 | max-width: 700px; 224 | margin: 0 auto; 225 | } 226 | .footer { 227 | padding: 2rem; 228 | background: var(--app-footer-bgColor); 229 | } 230 | } 231 | 232 | /** 233 | *========================================================================== 234 | * Titlebar Overrides 235 | *========================================================================== 236 | * 237 | * Here we override color, style, layout sizes for custom electron window 238 | */ 239 | 240 | .window-titlebar { 241 | background: var(--titlebar-bgcolor); 242 | height: auto; 243 | overflow: hidden; 244 | 245 | &-icon { 246 | min-height: 33px; 247 | img { 248 | border-radius: 50%; 249 | } 250 | } 251 | 252 | .window-title { 253 | color: var(--titlebar-title-color); 254 | } 255 | 256 | .menu-item { 257 | &.active .menu-title { 258 | background: var(--titlebar-menu-title-active-bgcolor); 259 | box-shadow: var(--titlebar-menu-title-shadow); 260 | color: var(--titlebar-color); 261 | border-color: var(--titlebar-menu-title-active-border-color); 262 | border-radius: 4px 4px 0 0; 263 | border-top-color: var(--app-accent-color); 264 | } 265 | } 266 | 267 | .menu-title { 268 | font-weight: normal; 269 | text-shadow: none; 270 | color: var(--titlebar-color); 271 | border-color: transparent; 272 | border-width: 1px 1px 0 1px; 273 | border-style: solid; 274 | padding: 2px 8px; 275 | margin-right: 2px; 276 | border-radius: 4px; 277 | 278 | &:hover { 279 | background-color: var(--titlebar-menu-title-hover-bgcolor); 280 | } 281 | } 282 | 283 | .menu-popup { 284 | display: none; 285 | position: fixed; 286 | background-color: var(--titlebar-popup-bgcolor); 287 | min-width: 70px; 288 | border: var(--titlebar-popup-border); 289 | border-top: 0; 290 | padding: 0.25rem 0; 291 | box-shadow: 4px 10px 10px #0000002e; 292 | z-index: 10000; 293 | border-radius: 0 6px 6px 6px; 294 | 295 | &-item { 296 | padding: 0.3125rem 1rem; 297 | 298 | &:hover { 299 | background: var(--titlebar-popup-item-hover-bgcolor); 300 | 301 | .popup-item-shortcut { 302 | color: var(--titlebar-popup-item-hover-shortcut-color); 303 | } 304 | 305 | .popup-item-name { 306 | color: var(--titlebar-popup-item-hover-color); 307 | } 308 | } 309 | } 310 | } 311 | 312 | .popup-item-separator { 313 | background: var(--titlebar-menu-separator-bgcolor); 314 | } 315 | 316 | .popup-item-name { 317 | padding-right: 2rem; 318 | color: var(--titlebar-popup-item-name-color); 319 | text-shadow: 0px 1px var(--titlebar-popup-item-name-shadow-color); 320 | } 321 | 322 | .popup-item-shortcut { 323 | color: var(--titlebar-popup-item-shortcut-color); 324 | text-shadow: none; 325 | letter-spacing: 0.5px; 326 | } 327 | 328 | &-controls.type-windows .control { 329 | color: var(--titlebar-color); 330 | font-family: Arial, Helvetica, sans-serif; 331 | 332 | &:hover { 333 | color: var(--titlebar-color); 334 | background: var(--titlebar-popup-item-hover-bgcolor); 335 | } 336 | 337 | &.close:hover { 338 | color: #fff; 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/renderer/components/Application.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import './Application.scss'; 3 | import { icons } from './Icons'; 4 | 5 | const Application: React.FC = () => { 6 | const [counter, setCounter] = useState(0); 7 | const [darkTheme, setDarkTheme] = useState(true); 8 | const [versions, setVersions] = useState>({}); 9 | 10 | /** 11 | * On component mount 12 | */ 13 | useEffect(() => { 14 | const useDarkTheme = parseInt(localStorage.getItem('dark-mode')); 15 | if (isNaN(useDarkTheme)) { 16 | setDarkTheme(true); 17 | } else if (useDarkTheme == 1) { 18 | setDarkTheme(true); 19 | } else if (useDarkTheme == 0) { 20 | setDarkTheme(false); 21 | } 22 | 23 | // Apply verisons 24 | const app = document.getElementById('app'); 25 | const versions = JSON.parse(app.getAttribute('data-versions')); 26 | setVersions(versions); 27 | }, []); 28 | 29 | /** 30 | * On Dark theme change 31 | */ 32 | useEffect(() => { 33 | if (darkTheme) { 34 | localStorage.setItem('dark-mode', '1'); 35 | document.body.classList.add('dark-mode'); 36 | } else { 37 | localStorage.setItem('dark-mode', '0'); 38 | document.body.classList.remove('dark-mode'); 39 | } 40 | }, [darkTheme]); 41 | 42 | /** 43 | * Toggle Theme 44 | */ 45 | function toggleTheme() { 46 | setDarkTheme(!darkTheme); 47 | } 48 | 49 | return ( 50 |
51 |
52 |
53 |

ERWT - Electron Boilerplate

54 |
55 |
56 |
57 | Robust boilerplate for Desktop Applications with Electron and 58 | ReactJS. 59 |
60 | Hot Reloading is used in this project for fast development 61 | experience. 62 |
63 | If you think the project is useful enough, just spread the word around! 64 |
65 |
66 |
67 |
68 |
69 | Electron 70 |
71 | {versions?.electron} 72 |
73 |
74 |
75 | ERWT 76 |
77 | {versions?.erwt} 78 |
79 |
80 |
81 | Typescript 82 |
83 | {versions?.typescript} 84 |
85 |
86 |
87 | Nodejs 88 |
89 | {versions?.node} 90 |
91 |
92 |
93 | React 94 |
95 | {versions?.react} 96 |
97 |
98 |
99 | Webpack 100 |
101 | {versions?.webpack} 102 |
103 |
104 |
105 | Chrome 106 |
107 | {versions?.chrome} 108 |
109 |
110 |
111 | License 112 |
113 | {versions?.license} 114 |
115 |
116 |
117 | 118 |
119 |
120 | 128 |       129 | 137 |       138 | 141 |
142 |
143 |
144 | ); 145 | }; 146 | 147 | export default Application; 148 | -------------------------------------------------------------------------------- /src/renderer/components/Icons.tsx: -------------------------------------------------------------------------------- 1 | import chrome from '@assets/icons/chrome.png'; 2 | import react from '@assets/icons/react.png'; 3 | import typescript from '@assets/icons/typescript.png'; 4 | import erwt from '@assets/icons/erwt.png'; 5 | import electron from '@assets/icons/electron.png'; 6 | import nodejs from '@assets/icons/nodejs.png'; 7 | import webpack from '@assets/icons/webpack.png'; 8 | import license from '@assets/icons/license.png'; 9 | 10 | export const icons = { 11 | chrome, 12 | react, 13 | typescript, 14 | erwt, 15 | electron, 16 | nodejs, 17 | webpack, 18 | license 19 | }; 20 | -------------------------------------------------------------------------------- /src/renderer/components/Theme.scss: -------------------------------------------------------------------------------- 1 | /* 2 | *========================================================================= 3 | * ERWT Dark Theme 4 | *========================================================================= 5 | * 6 | * Here we define the Dark Theme (stylesheet) for application. 7 | */ 8 | 9 | :root { 10 | // Application 11 | --app-accent-color: #32639f; 12 | --app-font-size: 16px; 13 | --app-font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, 14 | "Open Sans", "Helvetica Neue", sans-serif; 15 | --app-bgcolor: #202020; 16 | --app-border-color: #3b3f44; 17 | 18 | // Scrollbar 19 | --scroll-width: 10px; 20 | --scroll-track-bgcolor: #2f2f2f52; 21 | --scroll-thumb-bgcolor: rgba(59, 59, 59, 0.747); 22 | --scroll-thumb-hover-bgcolor: #555; 23 | 24 | // Selection 25 | --selection-bgcolor: var(--app-accent-color); 26 | --selection-color: #fff; 27 | 28 | // Button 29 | --button-color: #fff; 30 | --button-border: 1px solid #222424; 31 | --button-bgcolor: hsl(0, 0%, 22%); 32 | --button-hover-bgcolor: hsl(0, 0%, 24%); 33 | --button-active-bgcolor: hsl(0deg 0% 20%); 34 | --button-shadow-color: #00000078; 35 | 36 | // Titlebar 37 | --titlebar-bgcolor: hsl(0deg 0% 10%); 38 | --titlebar-color: hsl(0, 0%, 85%); 39 | --titlebar-title-color: hsl(0, 0%, 85%); 40 | --titlebar-menu-border-color: #2d2c2c; 41 | --titlebar-menu-title-hover-bgcolor: hsl(0deg 0% 12%); 42 | --titlebar-menu-title-active-bgcolor: hsl(0deg 0% 14%); 43 | --titlebar-menu-title-active-border-color: var(--titlebar-menu-border-color); 44 | --titlebar-menu-separator-bgcolor: var(--titlebar-menu-border-color); 45 | --titlebar-popup-bgcolor: hsl(0deg 0% 14%); 46 | --titlebar-popup-border: 1px solid var(--titlebar-menu-border-color); 47 | --titlebar-popup-shadow: 4px 10px 10px rgba(0, 0, 0, 0.2); 48 | --titlebar-popup-item-name-color: hsl(0, 0%, 75%); 49 | --titlebar-popup-item-shortcut-color: hsla(0, 0%, 55%, 0.8); 50 | --titlebar-popup-item-hover-color: hsl(0, 0%, 85%); 51 | --titlebar-popup-item-hover-bgcolor: hsl(0deg 0% 18%); 52 | --titlebar-popup-item-hover-shortcut-color: var(--app-accent-color); 53 | --titlebar-popup-item-name-shadow-color: #151515; 54 | 55 | // ERWT 56 | --erwt-heading-color: #dddddd; 57 | } 58 | 59 | /* 60 | *========================================================================= 61 | * ERWT Light Theme 62 | *========================================================================= 63 | * 64 | * Light theme for ERWT application. 65 | */ 66 | 67 | body:not(.dark-mode) { 68 | // Application 69 | --app-accent-color: #2f77ca; 70 | --app-bgcolor: #f1f0f0; 71 | 72 | // Selection 73 | --selection-bgcolor: var(--app-accent-color); 74 | --selection-color: #fff; 75 | 76 | // Scrollbar 77 | --scroll-track-bgcolor: #2f2f2f1f; 78 | --scroll-thumb-bgcolor: rgb(59 59 59 / 30%); 79 | --scroll-thumb-hover-bgcolor: var(--app-accent-color); 80 | 81 | // Button 82 | --button-border: 1px solid #eaeaea; 83 | --button-bgcolor: #fff; 84 | --button-color: #424242; 85 | --button-shadow-color: white; 86 | --button-hover-bgcolor: hsl(0, 0%, 98%); 87 | --button-active-bgcolor: hsl(0, 0%, 96%); 88 | 89 | // Titlebar 90 | --titlebar-bgcolor: #dcddde; 91 | --titlebar-color: #1f1f1f; 92 | --titlebar-title-color: var(--titlebar-color); 93 | --titlebar-menu-title-hover-bgcolor: #e6e6e6; 94 | --titlebar-popup-item-hover-bgcolor: hsl(210deg 16% 92%); 95 | --titlebar-popup-bgcolor: hsl(0deg 0% 99%); 96 | --titlebar-menu-title-active-bgcolor: hsl(0deg 0% 99%); 97 | --titlebar-menu-title-active-border-color: #d3d6d8; 98 | --titlebar-popup-border: 1px solid #d3d6d8; 99 | --titlebar-menu-separator-bgcolor: #d3d6d8; 100 | --titlebar-popup-item-hover-bgcolor: #d6dade; 101 | --titlebar-popup-item-name-color: #0e0e0e; 102 | --titlebar-popup-item-name-shadow-color: rgba(255, 255, 255, 0.8); 103 | --titlebar-popup-item-hover-color: #000000; 104 | --titlebar-popup-item-hover-shortcut-color: var(--app-accent-color); 105 | --titlebar-popup-item-hover-bgcolor: #e6e6e6; 106 | 107 | // ERWT 108 | --erwt-heading-color: #282828; 109 | 110 | // Overrides 111 | .main-teaser { 112 | background: #fff; 113 | color: #747474; 114 | 115 | &:after { 116 | box-shadow: none; 117 | } 118 | } 119 | 120 | .versions .item { 121 | color: #656565; 122 | background: #fff; 123 | box-shadow: 0 2px 4px #00000005; 124 | 125 | & > span { 126 | color: #999999; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.png'; 3 | declare module '*.jpg'; 4 | declare module '*.jpeg'; 5 | -------------------------------------------------------------------------------- /tools/forge/forge.config.js: -------------------------------------------------------------------------------- 1 | // Forge Configuration 2 | const path = require('path'); 3 | const rootDir = process.cwd(); 4 | 5 | module.exports = { 6 | // Packager Config 7 | packagerConfig: { 8 | // Create asar archive for main, renderer process files 9 | asar: true, 10 | // Set executable name 11 | executableName: 'Electron-React-Typescript Application', 12 | // Set application copyright 13 | appCopyright: 'Copyright (C) 2021 Darkhorse', 14 | // Set application icon 15 | icon: path.resolve('assets/images/appIcon.ico'), 16 | }, 17 | // Forge Makers 18 | makers: [ 19 | { 20 | // Squirrel.Windows is a no-prompt, no-hassle, no-admin method of installing 21 | // Windows applications and is therefore the most user friendly you can get. 22 | name: '@electron-forge/maker-squirrel', 23 | config: { 24 | name: 'electron-react-typescript-webpack-application', 25 | }, 26 | }, 27 | { 28 | // The Zip target builds basic .zip files containing your packaged application. 29 | // There are no platform specific dependencies for using this maker and it will run on any platform. 30 | name: '@electron-forge/maker-zip', 31 | platforms: ['darwin'], 32 | }, 33 | { 34 | // The deb target builds .deb packages, which are the standard package format for Debian-based 35 | // Linux distributions such as Ubuntu. 36 | name: '@electron-forge/maker-deb', 37 | config: {}, 38 | }, 39 | { 40 | // The RPM target builds .rpm files, which is the standard package format for 41 | // RedHat-based Linux distributions such as Fedora. 42 | name: '@electron-forge/maker-rpm', 43 | config: {}, 44 | }, 45 | ], 46 | // Forge Plugins 47 | plugins: [ 48 | { 49 | name: '@electron-forge/plugin-webpack', 50 | config: { 51 | // Fix content-security-policy error when image or video src isn't same origin 52 | // Remove 'unsafe-eval' to get rid of console warning in development mode. 53 | devContentSecurityPolicy: `default-src 'self' 'unsafe-inline' data:; script-src 'self' 'unsafe-inline' data:`, 54 | // Ports 55 | port: 3000, // Webpack Dev Server port 56 | loggerPort: 9000, // Logger port 57 | // Main process webpack configuration 58 | mainConfig: path.join(rootDir, 'tools/webpack/webpack.main.js'), 59 | // Renderer process webpack configuration 60 | renderer: { 61 | // Configuration file path 62 | config: path.join(rootDir, 'tools/webpack/webpack.renderer.js'), 63 | // Entrypoints of the application 64 | entryPoints: [ 65 | { 66 | // Window process name 67 | name: 'app_window', 68 | // React Hot Module Replacement (HMR) 69 | rhmr: 'react-hot-loader/patch', 70 | // HTML index file template 71 | html: path.join(rootDir, 'src/renderer/app.html'), 72 | // Renderer 73 | js: path.join(rootDir, 'src/renderer/appRenderer.tsx'), 74 | // Main Window 75 | // Preload 76 | preload: { 77 | js: path.join(rootDir, 'src/renderer/appPreload.tsx'), 78 | }, 79 | }, 80 | ], 81 | }, 82 | devServer: { 83 | liveReload: false, 84 | }, 85 | }, 86 | }, 87 | ], 88 | }; 89 | -------------------------------------------------------------------------------- /tools/webpack/webpack.aliases.js: -------------------------------------------------------------------------------- 1 | const { createWebpackAliases } = require('./webpack.helpers'); 2 | 3 | // Export aliases 4 | module.exports = createWebpackAliases({ 5 | '@assets': 'assets', 6 | '@components': 'src/renderer/components', 7 | '@common': 'src/common', 8 | '@main': 'src/main', 9 | '@renderer': 'src/renderer', 10 | '@src': 'src', 11 | '@misc': 'misc', 12 | }); 13 | -------------------------------------------------------------------------------- /tools/webpack/webpack.helpers.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const cwd = process.cwd(); 3 | 4 | 5 | function inDev() { 6 | return process.env.NODE_ENV == 'development'; 7 | } 8 | 9 | function createWebpackAliases (aliases) { 10 | const result = {}; 11 | for (const name in aliases) { 12 | result[name] = path.join(cwd, aliases[name]); 13 | } 14 | return result; 15 | } 16 | 17 | module.exports = { 18 | inDev, 19 | createWebpackAliases, 20 | }; 21 | -------------------------------------------------------------------------------- /tools/webpack/webpack.main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * This is the main entry point for your application, it's the first file 4 | * that runs in the main process. 5 | */ 6 | entry: ['./src/main/app.ts'], 7 | // Put your normal webpack config below here 8 | module: { 9 | rules: require('./webpack.rules'), 10 | }, 11 | resolve: { 12 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], 13 | alias: require('./webpack.aliases'), 14 | }, 15 | stats: 'minimal', 16 | }; 17 | -------------------------------------------------------------------------------- /tools/webpack/webpack.plugins.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 3 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 4 | const { inDev } = require('./webpack.helpers'); 5 | 6 | module.exports = [ 7 | new ForkTsCheckerWebpackPlugin(), 8 | inDev() && new webpack.HotModuleReplacementPlugin(), 9 | inDev() && new ReactRefreshWebpackPlugin(), 10 | ].filter(Boolean); 11 | -------------------------------------------------------------------------------- /tools/webpack/webpack.renderer.js: -------------------------------------------------------------------------------- 1 | const rules = require('./webpack.rules'); 2 | const plugins = require('./webpack.plugins'); 3 | 4 | module.exports = { 5 | module: { 6 | rules, 7 | }, 8 | plugins: plugins, 9 | resolve: { 10 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'], 11 | alias: { 12 | // Custom Aliases 13 | ...require('./webpack.aliases'), 14 | }, 15 | }, 16 | stats: 'minimal', 17 | /** 18 | * Fix: Enable inline-source-map to fix following: 19 | * Dev tools: unable to load source maps over custom protocol 20 | */ 21 | devtool: 'inline-source-map', 22 | }; 23 | -------------------------------------------------------------------------------- /tools/webpack/webpack.rules.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | // Add support for native node modules 4 | test: /native_modules\/.+\.node$/, 5 | use: 'node-loader', 6 | }, 7 | { 8 | test: /\.(m?js|node)$/, 9 | parser: { amd: false }, 10 | use: { 11 | loader: '@vercel/webpack-asset-relocator-loader', 12 | options: { 13 | outputAssetBase: 'native_modules', 14 | }, 15 | }, 16 | }, 17 | { 18 | // Typescript loader 19 | test: /\.tsx?$/, 20 | exclude: /(node_modules|\.webpack)/, 21 | use: { 22 | loader: 'ts-loader', 23 | options: { 24 | transpileOnly: true, 25 | }, 26 | }, 27 | }, 28 | { 29 | // CSS Loader 30 | test: /\.css$/, 31 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], 32 | }, 33 | { 34 | // SCSS (SASS) Loader 35 | test: /\.s[ac]ss$/i, 36 | use: [ 37 | { loader: 'style-loader' }, 38 | { loader: 'css-loader' }, 39 | { loader: 'sass-loader' }, 40 | ], 41 | }, 42 | { 43 | // Less loader 44 | test: /\.less$/, 45 | use: [ 46 | { loader: 'style-loader' }, 47 | { loader: 'css-loader' }, 48 | { loader: 'less-loader' }, 49 | ], 50 | }, 51 | { 52 | // Assets loader 53 | // More information here https://webpack.js.org/guides/asset-modules/ 54 | test: /\.(gif|jpe?g|tiff|png|webp|bmp|svg|eot|ttf|woff|woff2)$/i, 55 | type: 'asset', 56 | generator: { 57 | filename: 'assets/[hash][ext][query]', 58 | }, 59 | }, 60 | ]; 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "allowJs": true, 5 | "target": "ES6", 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "sourceMap": true, 11 | "baseUrl": ".", 12 | "outDir": "dist", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "paths": { 16 | "*": ["node_modules/*"], 17 | "@assets/*": ["./assets/*"], 18 | "@components/*": ["./src/renderer/components/*"], 19 | "@common/*": ["./src/common/*"], 20 | "@main/*": ["./src/main/*"], 21 | "@renderer/*": ["./src/renderer/*"], 22 | "@src/*": ["./src/*"], 23 | "@misc/*": ["./misc/*"], 24 | } 25 | }, 26 | "include": ["src/**/*", "tools/**/*"] 27 | } 28 | --------------------------------------------------------------------------------