├── .config ├── .eslintrc.js └── entitlements.mac.plist ├── .gitignore ├── .meta ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── public ├── dmgBackground-source.svg ├── dmgBackground.png ├── icon.icns ├── icon.ico ├── icon.png └── screenshot.png ├── scripts ├── common.ts ├── dev.ts ├── notarize.js ├── prod-esbuild.ts ├── prod.ts ├── run-electron.ts ├── run-vite.ts └── tsconfig.json ├── src ├── common │ └── .gitkeep ├── index.twstyled.css ├── main │ ├── index.ts │ └── tsconfig.json └── renderer │ ├── App.tsx │ ├── components │ └── top-bar.tsx │ ├── favicon.svg │ ├── index.css │ ├── index.html │ ├── logo.png │ ├── logo.svg │ ├── main.tsx │ └── tsconfig.json ├── tailwind.config.js ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.config/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // Sync Labs defaults 2 | // See https://github.com/synclabs-dev/eslint-config 3 | // Includes Typescript and prettier 4 | 5 | // FIX-DEPENDENCIES 6 | require.resolve('eslint') 7 | require.resolve('prettier') 8 | 9 | // This is a workaround for https://github.com/eslint/eslint/issues/3458 10 | require('@sync-labs/eslint-config/patch/modern-module-resolution') 11 | 12 | // The ESLint browser environment defines all browser globals as valid, 13 | // even though most people don't know some of them exist (e.g. `name` or `status`). 14 | // This is dangerous as it hides accidentally undefined variables. 15 | // We blacklist the globals that we deem potentially confusing. 16 | // To use them, explicitly reference them, e.g. `window.name` or `window.status`. 17 | 18 | module.exports = { 19 | root: true, 20 | extends: ["@sync-labs/eslint-config/profile/node"], // <---- put your profile string here 21 | parserOptions: { 22 | tsconfigRootDir: require('path').resolve(__dirname, '..'), 23 | "ecmaVersion": 2017, 24 | "env": { 25 | "es6": true 26 | }, 27 | "sourceType": "module", 28 | "allowImportExportEverywhere": true 29 | }, 30 | rules: { 31 | '@typescript-eslint/no-parameter-properties': 'off', 32 | '@typescript-eslint/no-use-before-define': 'off', 33 | '@typescript-eslint/no-explicit-any': 'off', 34 | '@typescript-eslint/explicit-function-return-type': 'off', 35 | '@typescript-eslint/ban-types': 'off', 36 | 'no-void': 'off', 37 | '@rushstack/no-new-null': 'off', 38 | 'react/jsx-uses-react': "off", 39 | 'react/react-in-jsx-scope': "off" 40 | } 41 | } -------------------------------------------------------------------------------- /.config/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .twstyled-cache/ 7 | 8 | 9 | # Secrets 10 | .env-secrets.json 11 | 12 | # Workspaces 13 | -------------------------------------------------------------------------------- /.meta: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "lib-twstyled": "git@github.com:twstyled/twstyled.git" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.useIgnoreFiles": false, 3 | "search.exclude": { 4 | "**/.next": true, 5 | "**/build": true, 6 | "**/node_modules": true, 7 | "**/out": true, 8 | "**/script.js": true, 9 | "**/*-cache": true, 10 | "**/dist": true, 11 | "yarn.lock": true, 12 | "**/*.mdx": true 13 | }, 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll.eslint": true 16 | }, 17 | "editor.formatOnSave": false, 18 | "editor.codeActionsOnSaveTimeout": 6000, 19 | "css.lint.unknownAtRules": "ignore", 20 | "eslint.options": { 21 | "configFile": "./.config/.eslintrc.js" 22 | }, 23 | "eslint.lintTask.options": "-c ${workspaceRoot}/.config/.eslintrc.js --ext .ts,.js,.tsx .", 24 | "eslint.lintTask.enable": true, 25 | "[javascript]": { 26 | "editor.formatOnSave": true 27 | }, 28 | "[javascriptreact]": { 29 | "editor.formatOnSave": false 30 | }, 31 | "[typescript]": { 32 | "editor.formatOnSave": false 33 | }, 34 | "[typescriptreact]": { 35 | "editor.formatOnSave": false 36 | }, 37 | "[json]": { 38 | "editor.formatOnSave": true 39 | }, 40 | "eslint.validate": [ 41 | "javascript", 42 | "javascriptreact", 43 | "typescript", 44 | "typescriptreact" 45 | ], 46 | "eslint.alwaysShowStatus": true, 47 | "files.eol": "\n", 48 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 49 | "typescript.tsc.autoDetect": "off", 50 | "tailwindCSS.includeLanguages": { 51 | "plaintext": "html", 52 | "javascript": "javascriptreact", 53 | "typescript": "javascript", 54 | "typescriptreact": "javascriptreact" 55 | } 56 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 twstyled contributors 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron 11 + TypeScript 4.0 + Vite 2 + React 17 + Tailwind 2 + twstyled starter 2 | 3 | ![Electron + Vite + React Starter](https://github.com/twstyled/electron-vite-react/blob/main/public/screenshot.png) 4 | 5 | Blazing fast Electron starter including 6 | - [x] `Vite` for next generation frontend tooling 7 | - [x] `Typescript` 8 | - [x] `ESBuild` for building all assets including the main process 9 | - [x] `React` as the front end framework 10 | - [x] `Tailwind CSS` for styling without templates 11 | - [x] `twstyled` tailwind compiler for no PostCSS processing and full-featured CSS in JS 12 | - [x] `Framer Motion` for animation transitions 13 | - [x] `React fast refresh` for hot module reloading 14 | - [x] `electron-builder` and `electron-notarize` for distribution release 15 | - [x] Zero dependency on Vue, tsc or styled-components, emotion or other runtime CSS in JS (but supports all the same API) 16 | 17 | Configured with best practices. 18 | 19 | ## Installation 20 | 21 | `yarn` 22 | 23 | ## Development 24 | 25 | `yarn dev` 26 | 27 | ## Build 28 | 29 | `yarn build` 30 | 31 | ## Release 32 | 33 | Add any configuration to the `build` section of `package.json`, add an `.env-secrets.json` file in the `.config` folder with any environment secrets that you need for your publisher, and then run 34 | 35 | `yarn publish` 36 | 37 | # Prior art 38 | 39 | Inspired by https://github.com/appinteractive/electron-vite-tailwind-starter and https://github.com/maxstue/vite-reactts-electron-starter 40 | 41 | # License 42 | 43 | MIT 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-vite-react", 3 | "description": "This Starter utilizes Electron, TypeScript, Vite, React, Tailwind CSS with twstyled CSS in JS. It tries to adhere to best practices.", 4 | "version": "1.0.1", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "clean": "rimraf yarn.lock && rimraf ./**/.next && rimraf dist && rimraf ./**/out && rimraf {**/,}node_modules", 9 | "dev": "cross-env NODE_ENV=development node -r esbuild-register ./scripts/dev.ts", 10 | "preview": "cross-env NODE_ENV=production electron ./dist/main", 11 | "dev:renderer": "vite", 12 | "dev:main": "cross-env NODE_ENV=development node -r esbuild-register ./scripts/dev.ts", 13 | "build": "rimraf dist && vite build && npm run prod:esbuild", 14 | "prod:esbuild": "cross-env NODE_ENV=production node -r esbuild-register ./scripts/prod.ts", 15 | "prod:tsc": "tsc --project ./src/main/tsconfig.json", 16 | "publish": "concurrently \"npm:publish:mac\" \"npm:publish:win\"", 17 | "publish:mac": "env-cmd -r ./.config/.env-secrets.json -e production electron-builder --macos", 18 | "publish:win": "env-cmd -r ./.config/.env-secrets.json -e production electron-builder --win" 19 | }, 20 | "dependencies": { 21 | "@twstyled/core": "^3.2.4", 22 | "@twstyled/theme": "^3.2.4", 23 | "@twstyled/util": "^3.2.4", 24 | "electron-context-menu": "^2.5.0", 25 | "electron-window-state": "^5.0.3", 26 | "framer-motion": "3.7.0", 27 | "react": "^17.0.0", 28 | "react-dom": "^17.0.0", 29 | "tailwindcss": "^2.0.3" 30 | }, 31 | "devDependencies": { 32 | "@sync-labs/eslint-config": "^2.2.6", 33 | "@types/node": "^14.14.31", 34 | "@types/react": "^17.0.0", 35 | "@types/react-dom": "^17.0.0", 36 | "@vitejs/plugin-react-refresh": "^1.1.0", 37 | "chalk": "^4.1.0", 38 | "concurrently": "^6.0.0", 39 | "cross-env": "^7.0.3", 40 | "env-cmd": "^10.1.0", 41 | "electron": "^11.3.0", 42 | "electron-builder": "^22.9.1", 43 | "electron-notarize": "^1.0.0", 44 | "esbuild": "^0.8.53", 45 | "esbuild-register": "^2.0.0", 46 | "eslint": "^7.20.0", 47 | "prettier": "^2.2.1", 48 | "rimraf": "^3.0.2", 49 | "typescript": "^4.1.2", 50 | "vite": "^2.0.4", 51 | "vite-plugin-twstyled": "^3.2.4" 52 | }, 53 | "resolutions": { 54 | "@linaria/babel-preset": "npm:@twstyled/linaria-babel-preset@3.0.0-beta.1", 55 | "@linaria/preeval": "npm:@twstyled/linaria-preeval@3.0.0-beta.1" 56 | }, 57 | "author": "twstyled contributors", 58 | "browserslist": [ 59 | "last 1 electron version" 60 | ], 61 | "build": { 62 | "directories": { 63 | "output": "dist", 64 | "buildResources": "public" 65 | }, 66 | "appId": "dev.twstyled.app", 67 | "productName": "twstyled-app", 68 | "afterSign": "scripts/notarize.js", 69 | "mac": { 70 | "category": "public.app-category.developer-tools", 71 | "extendInfo": { 72 | "NSUserNotificationAlertStyle": "alert" 73 | }, 74 | "hardenedRuntime": true, 75 | "gatekeeperAssess": false, 76 | "entitlements": ".config/entitlements.mac.plist", 77 | "entitlementsInherit": ".config/entitlements.mac.plist", 78 | "publish": [ 79 | { 80 | "provider": "generic", 81 | "url": "https://files.twstyled.com/releases/${name}/${os}/" 82 | } 83 | ] 84 | }, 85 | "dmg": { 86 | "background": "public/dmgBackground.png", 87 | "icon": "icon.icns", 88 | "iconSize": 155, 89 | "window": { 90 | "width": 660, 91 | "height": 400 92 | }, 93 | "contents": [ 94 | { 95 | "x": 123, 96 | "y": 172 97 | }, 98 | { 99 | "x": 539, 100 | "y": 168, 101 | "type": "link", 102 | "path": "/Applications" 103 | } 104 | ] 105 | }, 106 | "win": { 107 | "publisherName": "twstyled contributors", 108 | "sign": null, 109 | "publish": [ 110 | { 111 | "provider": "generic", 112 | "url": "https://files.twstyled.com/releases/${name}/${os}/" 113 | } 114 | ] 115 | }, 116 | "asar": false, 117 | "extraMetadata": { 118 | "main": "main/index.js" 119 | }, 120 | "files": [ 121 | { 122 | "filter": [ 123 | "package.json", 124 | "tailwind.config.js" 125 | ] 126 | }, 127 | { 128 | "from": "./dist", 129 | "to": "./", 130 | "filter": [ 131 | "index.html" 132 | ] 133 | }, 134 | { 135 | "from": "./dist/main", 136 | "to": "./main" 137 | }, 138 | { 139 | "from": "./dist/assets", 140 | "to": "./assets" 141 | } 142 | ] 143 | }, 144 | "workspaces": [ 145 | "lib-twstyled/packages/*" 146 | ] 147 | } -------------------------------------------------------------------------------- /public/dmgBackground-source.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | dragTo 12 | 13 | 14 | ( 15 | 16 | 17 | ( 18 | 19 | 20 | ) 21 | 22 | 23 | ) 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/dmgBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twstyled/electron-vite-react/045bf9642e8e2777c32c62bf937078c7eb061963/public/dmgBackground.png -------------------------------------------------------------------------------- /public/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twstyled/electron-vite-react/045bf9642e8e2777c32c62bf937078c7eb061963/public/icon.icns -------------------------------------------------------------------------------- /public/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twstyled/electron-vite-react/045bf9642e8e2777c32c62bf937078c7eb061963/public/icon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twstyled/electron-vite-react/045bf9642e8e2777c32c62bf937078c7eb061963/public/icon.png -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twstyled/electron-vite-react/045bf9642e8e2777c32c62bf937078c7eb061963/public/screenshot.png -------------------------------------------------------------------------------- /scripts/common.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os' 2 | import * as path from 'path' 3 | import * as chalk from 'chalk' 4 | 5 | export type WatchMain = ( 6 | reportError: (errs: CompileError[]) => void, 7 | buildStart: () => void, 8 | buildComplete: (dir: string) => void, 9 | notFoundTSConfig: () => void 10 | ) => void 11 | 12 | export const srcPath = path.join(process.cwd(), './src') 13 | export const mainPath = path.join(process.cwd(), './src/main') 14 | export const outDir = path.join(process.cwd(), './dist') 15 | export const outDirMain = path.join(process.cwd(), './dist/main') 16 | export const entryPath = path.join(mainPath, 'index.ts') 17 | 18 | export const consoleMessagePrefix = '[script]' 19 | export const consoleViteMessagePrefix = '[vite]' 20 | 21 | export const cannotFoundTSConfigMessage = 22 | "Could not find a valid 'tsconfig.json'." 23 | export const startMessage = chalk.cyan( 24 | `${consoleMessagePrefix} Start compile main process...` 25 | ) 26 | export const finishMessageDev = chalk.green( 27 | `${consoleMessagePrefix} Finished compiling. Rerun electron main process...` 28 | ) 29 | export const finishMessageProd = chalk.green( 30 | `${consoleMessagePrefix} Finished production build.` 31 | ) 32 | 33 | export interface CompileError { 34 | location: { 35 | column: number 36 | file: string 37 | length: number 38 | line: number 39 | lineText: string 40 | } 41 | message: string 42 | } 43 | 44 | function repeatString(char: string, len: number): string { 45 | return Array(len).fill(char).join('') 46 | } 47 | 48 | function formatCompileError(error: CompileError): string { 49 | const pathMessage = 50 | chalk.cyan(error.location.file) + 51 | ':' + 52 | chalk.yellow(error.location.line) + 53 | ':' + 54 | chalk.yellow(error.location.column) 55 | const categoryMessage = chalk.red('error:') 56 | 57 | const code = 58 | chalk.gray(error.location.line) + 59 | ' ' + 60 | error.location.lineText + 61 | os.EOL + 62 | repeatString( 63 | ' ', 64 | error.location.column + `${error.location.line}`.length + 1 + 1 65 | ) + 66 | chalk.red(repeatString('~', error.location.length)) + 67 | repeatString( 68 | ' ', 69 | error.location.lineText.length - 70 | error.location.column - 71 | error.location.length 72 | ) 73 | 74 | return `${pathMessage} - ${categoryMessage} ${error.message} ${os.EOL} ${code}` 75 | } 76 | 77 | export function formatDiagnosticsMessage(errors: CompileError[]): string { 78 | const messages = errors.map((e) => formatCompileError(e)) 79 | const errorMessage = `Found ${errors.length} errors. Watching for file changes.` 80 | 81 | let diagnosticDetail = '' 82 | messages.forEach((item, index, { length }) => { 83 | diagnosticDetail += item 84 | .split(os.EOL) 85 | .map((i) => ' ' + i) 86 | .join(os.EOL) 87 | if (index + 1 !== length) { 88 | diagnosticDetail += os.EOL 89 | } 90 | }) 91 | 92 | const res = 93 | chalk.rgb( 94 | 255, 95 | 161, 96 | 237 97 | )(`${consoleMessagePrefix} Some typescript compilation errors occurred:`) + 98 | '\n' + 99 | diagnosticDetail + 100 | '\n' + 101 | chalk.rgb(255, 161, 237)(errorMessage) 102 | 103 | return res 104 | } 105 | -------------------------------------------------------------------------------- /scripts/dev.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | import * as esbuild from 'esbuild' 4 | import startViteServer from './run-vite' 5 | import startElectron from './run-electron' 6 | 7 | import { 8 | cannotFoundTSConfigMessage, 9 | CompileError, 10 | finishMessageDev, 11 | formatDiagnosticsMessage, 12 | startMessage, 13 | mainPath, 14 | outDir, 15 | entryPath 16 | } from './common' 17 | 18 | const chalk = require('chalk') 19 | 20 | function reportError(errors: CompileError[]) { 21 | const reportingMessage = formatDiagnosticsMessage(errors) 22 | console.error(reportingMessage) 23 | } 24 | 25 | function buildStart() { 26 | console.log(startMessage) 27 | } 28 | 29 | async function electronClosed() { 30 | if (viteClose) { 31 | await viteClose() 32 | } 33 | } 34 | 35 | function buildComplete(dir: string) { 36 | console.log(finishMessageDev) 37 | void startElectron(dir, electronClosed) 38 | } 39 | 40 | function notFoundTSConfig() { 41 | console.error(chalk.red(cannotFoundTSConfigMessage)) 42 | process.exit() 43 | } 44 | 45 | let viteClose: () => Promise 46 | 47 | async function main() { 48 | // Start vite server 49 | viteClose = await startViteServer() 50 | // Start dev for main process 51 | void esDev(reportError, buildStart, buildComplete, notFoundTSConfig) 52 | } 53 | 54 | void main() 55 | 56 | // 57 | // SUPPORTING BUILD SCRIPT 58 | // 59 | 60 | function transformErrors(error: esbuild.BuildFailure): CompileError[] { 61 | const errors = error.errors.map( 62 | (e): CompileError => { 63 | return { 64 | location: e.location, 65 | message: e.text 66 | } 67 | } 68 | ) 69 | return errors 70 | } 71 | 72 | async function esDev( 73 | reportError: { (errors: CompileError[]): void; (arg0: CompileError[]): void }, 74 | buildStart: () => void, 75 | buildComplete: { (dir: string): void; (arg0: string): void }, 76 | notFoundTSConfig: { (): void; (): void } 77 | ) { 78 | const tsconfigPath = path.join(mainPath, 'tsconfig.json') 79 | if (!fs.existsSync(tsconfigPath)) { 80 | notFoundTSConfig() 81 | } 82 | 83 | try { 84 | await esbuild.build({ 85 | outdir: outDir, 86 | entryPoints: [entryPath], 87 | tsconfig: tsconfigPath, 88 | format: 'cjs', 89 | logLevel: 'silent', 90 | errorLimit: 0, 91 | incremental: true, 92 | platform: 'node', 93 | sourcemap: true, 94 | watch: { 95 | onRebuild: (error) => { 96 | if (error) { 97 | reportError(transformErrors(error)) 98 | } else { 99 | buildComplete(outDir) 100 | } 101 | } 102 | } 103 | }) 104 | buildComplete(outDir) 105 | } catch (e) { 106 | if (!!e.errors && !!e.errors.length && e.errors.length > 0) { 107 | const error = e as esbuild.BuildFailure 108 | reportError(transformErrors(error)) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('electron-notarize') 2 | 3 | exports.default = async function notarizing(context) { 4 | 5 | return 6 | const { electronPlatformName, appOutDir } = context 7 | if (electronPlatformName !== 'darwin') { 8 | return 9 | } 10 | 11 | // // .replace(new RegExp('\\n', 'g'), '\n'), 12 | 13 | const appName = context.packager.appInfo.productFilename 14 | 15 | await notarize({ 16 | appBundleId: 'com.twstyled.starter', 17 | appPath: `${appOutDir}/${appName}.app`, 18 | appleApiKey: process.env.AC_API_KEY, 19 | appleApiIssuer: process.env.AC_API_ISSUER 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /scripts/prod-esbuild.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build main process using ESBuild 3 | */ 4 | import * as path from 'path' 5 | import * as fs from 'fs' 6 | import * as esbuild from 'esbuild' 7 | import { CompileError, mainPath, outDirMain, entryPath } from './common' 8 | 9 | function transformErrors(error: esbuild.BuildFailure): CompileError[] { 10 | const errors = error.errors.map( 11 | (e): CompileError => { 12 | return { 13 | location: e.location, 14 | message: e.text 15 | } 16 | } 17 | ) 18 | return errors 19 | } 20 | 21 | export default async ( 22 | reportError, 23 | buildStart, 24 | buildComplete, 25 | notFoundTSConfig 26 | ) => { 27 | const tsconfigPath = path.join(mainPath, 'tsconfig.json') 28 | if (!fs.existsSync(tsconfigPath)) { 29 | notFoundTSConfig() 30 | } 31 | 32 | try { 33 | await esbuild.build({ 34 | outdir: outDirMain, 35 | entryPoints: [entryPath], 36 | tsconfig: tsconfigPath, 37 | format: 'cjs', 38 | logLevel: 'info', 39 | errorLimit: 0, 40 | incremental: true, 41 | platform: 'node', 42 | sourcemap: false, 43 | watch: false 44 | }) 45 | buildComplete(outDirMain) 46 | } catch (e) { 47 | if (!!e.errors && !!e.errors.length && e.errors.length > 0) { 48 | const error = e as esbuild.BuildFailure 49 | reportError(transformErrors(error)) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build main process using ESBuild 3 | */ 4 | import * as path from 'path' 5 | import * as fs from 'fs' 6 | import * as esbuild from 'esbuild' 7 | import { 8 | cannotFoundTSConfigMessage, 9 | finishMessageProd, 10 | formatDiagnosticsMessage, 11 | startMessage, 12 | CompileError, 13 | mainPath, 14 | outDirMain, 15 | entryPath 16 | } from './common' 17 | 18 | function reportError(errors: CompileError[]) { 19 | const reportingMessage = formatDiagnosticsMessage(errors) 20 | console.error(reportingMessage) 21 | } 22 | 23 | function buildStart() { 24 | console.log(startMessage) 25 | } 26 | 27 | function buildComplete(dir: string) { 28 | console.log(finishMessageProd) 29 | process.exit() 30 | } 31 | 32 | function notFoundTSConfig() { 33 | console.error(chalk.red(cannotFoundTSConfigMessage)) 34 | process.exit() 35 | } 36 | 37 | async function main() { 38 | // Start dev for main process 39 | void esProd(reportError, buildStart, buildComplete, notFoundTSConfig) 40 | } 41 | 42 | void main() 43 | 44 | const chalk = require('chalk') 45 | 46 | function transformErrors(error: esbuild.BuildFailure): CompileError[] { 47 | const errors = error.errors.map( 48 | (e): CompileError => { 49 | return { 50 | location: e.location, 51 | message: e.text 52 | } 53 | } 54 | ) 55 | return errors 56 | } 57 | 58 | // 59 | // SUPPORTING BUILD SCRIPT 60 | // 61 | async function esProd( 62 | reportError: { (errors: CompileError[]): void; (arg0: CompileError[]): void }, 63 | buildStart: () => void, 64 | buildComplete: { (dir: string): void; (arg0: string): void }, 65 | notFoundTSConfig: { (): void; (): void } 66 | ) { 67 | const tsconfigPath = path.join(mainPath, 'tsconfig.json') 68 | if (!fs.existsSync(tsconfigPath)) { 69 | notFoundTSConfig() 70 | } 71 | 72 | try { 73 | await esbuild.build({ 74 | outdir: outDirMain, 75 | entryPoints: [entryPath], 76 | tsconfig: tsconfigPath, 77 | format: 'cjs', 78 | logLevel: 'info', 79 | errorLimit: 0, 80 | incremental: true, 81 | platform: 'node', 82 | sourcemap: false, 83 | watch: false 84 | }) 85 | buildComplete(outDirMain) 86 | } catch (e) { 87 | if (!!e.errors && !!e.errors.length && e.errors.length > 0) { 88 | const error = e as esbuild.BuildFailure 89 | reportError(transformErrors(error)) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /scripts/run-electron.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process' 2 | import * as stream from 'stream' 3 | import chalk = require('chalk') 4 | 5 | const electron = require('electron') 6 | 7 | let electronProcess: childProcess.ChildProcess | null = null 8 | 9 | const removeJunkTransformOptions: stream.TransformOptions = { 10 | decodeStrings: false, 11 | transform: (chunk, encoding, done) => { 12 | const source = chunk.toString() 13 | // Example: 2018-08-10 22:48:42.866 Electron[90311:4883863] *** WARNING: Textured window 14 | if ( 15 | /\d+-\d+-\d+ \d+:\d+:\d+\.\d+ Electron(?: Helper)?\[\d+:\d+] /.test( 16 | source 17 | ) 18 | ) { 19 | return false 20 | } 21 | // Example: [90789:0810/225804.894349:ERROR:CONSOLE(105)] "Uncaught (in promise) Error: Could not instantiate: ProductRegistryImpl.Registry", source: chrome-devtools://devtools/bundled/inspector.js (105) 22 | if (/\[\d+:\d+\/|\d+\.\d+:ERROR:CONSOLE\(\d+\)\]/.test(source)) { 23 | return false 24 | } 25 | // Example: ALSA lib confmisc.c:767:(parse_card) cannot find card '0' 26 | if (/ALSA lib [a-z]+\.c:\d+:\([a-z_]+\)/.test(source)) { 27 | return false 28 | } 29 | done(null, chunk) 30 | } 31 | } 32 | 33 | function delay(duration: number): Promise { 34 | return new Promise((resolve) => { 35 | setTimeout(() => { 36 | resolve() 37 | }, duration) 38 | }) 39 | } 40 | 41 | let exitByScripts = false 42 | export default async function startElectron( 43 | path: string, 44 | onClose: () => Promise 45 | ) { 46 | if (electronProcess) { 47 | process.kill(electronProcess.pid) 48 | exitByScripts = true 49 | electronProcess = null 50 | await delay(500) 51 | } 52 | 53 | // eslint-disable-next-line require-atomic-updates 54 | electronProcess = childProcess.spawn(electron, [path]) 55 | electronProcess.on('exit', async (code) => { 56 | if (!exitByScripts) { 57 | console.log(chalk.gray(`Electron exited with code ${code}`)) 58 | await onClose() 59 | process.exit() 60 | } 61 | // eslint-disable-next-line require-atomic-updates 62 | exitByScripts = true 63 | }) 64 | 65 | const removeElectronLoggerJunkOut = new stream.Transform( 66 | removeJunkTransformOptions 67 | ) 68 | const removeElectronLoggerJunkErr = new stream.Transform( 69 | removeJunkTransformOptions 70 | ) 71 | electronProcess.stdout.pipe(removeElectronLoggerJunkOut).pipe(process.stdout) 72 | electronProcess.stderr.pipe(removeElectronLoggerJunkErr).pipe(process.stderr) 73 | } 74 | -------------------------------------------------------------------------------- /scripts/run-vite.ts: -------------------------------------------------------------------------------- 1 | import { createServer, InlineConfig, Plugin } from 'vite' 2 | import * as chalk from 'chalk' 3 | import config from '../vite.config' 4 | import { consoleViteMessagePrefix, srcPath } from './common' 5 | 6 | function LoggerPlugin(): Plugin { 7 | return { 8 | name: 'electron-scripts-logger', 9 | handleHotUpdate: (ctx) => { 10 | for (const file of ctx.modules) { 11 | const path = file.file.replace(srcPath, '') 12 | console.log( 13 | chalk.yellow(consoleViteMessagePrefix), 14 | chalk.yellow('hmr update'), 15 | chalk.grey(path) 16 | ) 17 | } 18 | return ctx.modules 19 | } 20 | } 21 | } 22 | 23 | export default async function startViteServer(): Promise<() => Promise> { 24 | const cfg = config as InlineConfig 25 | const server = await createServer({ 26 | ...cfg, 27 | configFile: false, 28 | logLevel: 'silent', 29 | plugins: [...(cfg.plugins ?? []), LoggerPlugin()] 30 | }) 31 | await server.listen() 32 | const address = server.httpServer.address() 33 | if (typeof address === 'object') { 34 | const port = address.port 35 | console.log( 36 | chalk.green(consoleViteMessagePrefix), 37 | chalk.green(`Dev server running at: localhost:${port}`) 38 | ) 39 | } 40 | return async () => { 41 | await server.close() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2017" 5 | }, 6 | "include": ["./**/*"] 7 | } -------------------------------------------------------------------------------- /src/common/.gitkeep: -------------------------------------------------------------------------------- 1 | replace this folder with any files you want to include from both main and renderer 2 | 3 | ``` js 4 | import ____ from '@/common/util' 5 | ``` -------------------------------------------------------------------------------- /src/index.twstyled.css: -------------------------------------------------------------------------------- 1 | /*! Generated with twstyled | https://github.com/twstyled/twstyled */ 2 | /*! tailwindcss v2.0.3 | MIT License | https://tailwindcss.com *//*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize *//* 3 | Document 4 | ======== 5 | *//** 6 | Use a better box model (opinionated). 7 | */*, 8 | *::before, 9 | *::after { 10 | box-sizing: border-box; 11 | }/** 12 | Use a more readable tab size (opinionated). 13 | */:root { 14 | -moz-tab-size: 4; 15 | tab-size: 4; 16 | }/** 17 | 1. Correct the line height in all browsers. 18 | 2. Prevent adjustments of font size after orientation changes in iOS. 19 | */html { 20 | line-height: 1.15; /* 1 */ 21 | -webkit-text-size-adjust: 100%; /* 2 */ 22 | }/* 23 | Sections 24 | ======== 25 | *//** 26 | Remove the margin in all browsers. 27 | */body { 28 | margin: 0; 29 | }/** 30 | Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 31 | */body { 32 | font-family: 33 | system-ui, 34 | -apple-system, /* Firefox supports this but not yet `system-ui` */ 35 | 'Segoe UI', 36 | Roboto, 37 | Helvetica, 38 | Arial, 39 | sans-serif, 40 | 'Apple Color Emoji', 41 | 'Segoe UI Emoji'; 42 | }/* 43 | Grouping content 44 | ================ 45 | *//** 46 | 1. Add the correct height in Firefox. 47 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 48 | */hr { 49 | height: 0; /* 1 */ 50 | color: inherit; /* 2 */ 51 | }/* 52 | Text-level semantics 53 | ==================== 54 | *//** 55 | Add the correct text decoration in Chrome, Edge, and Safari. 56 | */abbr[title] { 57 | text-decoration: underline dotted; 58 | }/** 59 | Add the correct font weight in Edge and Safari. 60 | */b, 61 | strong { 62 | font-weight: bolder; 63 | }/** 64 | 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 65 | 2. Correct the odd 'em' font sizing in all browsers. 66 | */code, 67 | kbd, 68 | samp, 69 | pre { 70 | font-family: 71 | ui-monospace, 72 | SFMono-Regular, 73 | Consolas, 74 | 'Liberation Mono', 75 | Menlo, 76 | monospace; /* 1 */ 77 | font-size: 1em; /* 2 */ 78 | }/** 79 | Add the correct font size in all browsers. 80 | */small { 81 | font-size: 80%; 82 | }/** 83 | Prevent 'sub' and 'sup' elements from affecting the line height in all browsers. 84 | */sub, 85 | sup { 86 | font-size: 75%; 87 | line-height: 0; 88 | position: relative; 89 | vertical-align: baseline; 90 | }sub { 91 | bottom: -0.25em; 92 | }sup { 93 | top: -0.5em; 94 | }/* 95 | Tabular data 96 | ============ 97 | *//** 98 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 99 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 100 | */table { 101 | text-indent: 0; /* 1 */ 102 | border-color: inherit; /* 2 */ 103 | }/* 104 | Forms 105 | ===== 106 | *//** 107 | 1. Change the font styles in all browsers. 108 | 2. Remove the margin in Firefox and Safari. 109 | */button, 110 | input, 111 | optgroup, 112 | select, 113 | textarea { 114 | font-family: inherit; /* 1 */ 115 | font-size: 100%; /* 1 */ 116 | line-height: 1.15; /* 1 */ 117 | margin: 0; /* 2 */ 118 | }/** 119 | Remove the inheritance of text transform in Edge and Firefox. 120 | 1. Remove the inheritance of text transform in Firefox. 121 | */button, 122 | select { /* 1 */ 123 | text-transform: none; 124 | }/** 125 | Correct the inability to style clickable types in iOS and Safari. 126 | */button, 127 | [type='button'], 128 | [type='reset'], 129 | [type='submit'] { 130 | -webkit-appearance: button; 131 | }/** 132 | Remove the inner border and padding in Firefox. 133 | */::-moz-focus-inner { 134 | border-style: none; 135 | padding: 0; 136 | }/** 137 | Restore the focus styles unset by the previous rule. 138 | */:-moz-focusring { 139 | outline: 1px dotted ButtonText; 140 | }/** 141 | Remove the additional ':invalid' styles in Firefox. 142 | See: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737 143 | */:-moz-ui-invalid { 144 | box-shadow: none; 145 | }/** 146 | Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers. 147 | */legend { 148 | padding: 0; 149 | }/** 150 | Add the correct vertical alignment in Chrome and Firefox. 151 | */progress { 152 | vertical-align: baseline; 153 | }/** 154 | Correct the cursor style of increment and decrement buttons in Safari. 155 | */::-webkit-inner-spin-button, 156 | ::-webkit-outer-spin-button { 157 | height: auto; 158 | }/** 159 | 1. Correct the odd appearance in Chrome and Safari. 160 | 2. Correct the outline style in Safari. 161 | */[type='search'] { 162 | -webkit-appearance: textfield; /* 1 */ 163 | outline-offset: -2px; /* 2 */ 164 | }/** 165 | Remove the inner padding in Chrome and Safari on macOS. 166 | */::-webkit-search-decoration { 167 | -webkit-appearance: none; 168 | }/** 169 | 1. Correct the inability to style clickable types in iOS and Safari. 170 | 2. Change font properties to 'inherit' in Safari. 171 | */::-webkit-file-upload-button { 172 | -webkit-appearance: button; /* 1 */ 173 | font: inherit; /* 2 */ 174 | }/* 175 | Interactive 176 | =========== 177 | *//* 178 | Add the correct display in Chrome and Safari. 179 | */summary { 180 | display: list-item; 181 | }/** 182 | * Manually forked from SUIT CSS Base: https://github.com/suitcss/base 183 | * A thin layer on top of normalize.css that provides a starting point more 184 | * suitable for web applications. 185 | *//** 186 | * Removes the default spacing and border for appropriate elements. 187 | */blockquote, 188 | dl, 189 | dd, 190 | h1, 191 | h2, 192 | h3, 193 | h4, 194 | h5, 195 | h6, 196 | hr, 197 | figure, 198 | p, 199 | pre { 200 | margin: 0; 201 | }button { 202 | background-color: transparent; 203 | background-image: none; 204 | }/** 205 | * Work around a Firefox/IE bug where the transparent `button` background 206 | * results in a loss of the default `button` focus styles. 207 | */button:focus { 208 | outline: 1px dotted; 209 | outline: 5px auto -webkit-focus-ring-color; 210 | }fieldset { 211 | margin: 0; 212 | padding: 0; 213 | }ol, 214 | ul { 215 | list-style: none; 216 | margin: 0; 217 | padding: 0; 218 | }/** 219 | * Tailwind custom reset styles 220 | *//** 221 | * 1. Use the user's configured `sans` font-family (with Tailwind's default 222 | * sans-serif font stack as a fallback) as a sane default. 223 | * 2. Use Tailwind's default "normal" line-height so the user isn't forced 224 | * to override it to ensure consistency even when using the default theme. 225 | */html { 226 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */ 227 | line-height: 1.5; /* 2 */ 228 | }/** 229 | * Inherit font-family and line-height from `html` so users can set them as 230 | * a class directly on the `html` element. 231 | */body { 232 | font-family: inherit; 233 | line-height: inherit; 234 | }/** 235 | * 1. Prevent padding and border from affecting element width. 236 | * 237 | * We used to set this in the html element and inherit from 238 | * the parent element for everything else. This caused issues 239 | * in shadow-dom-enhanced elements like
where the content 240 | * is wrapped by a div with box-sizing set to `content-box`. 241 | * 242 | * https://github.com/mozdevs/cssremedy/issues/4 243 | * 244 | * 245 | * 2. Allow adding a border to an element by just adding a border-width. 246 | * 247 | * By default, the way the browser specifies that an element should have no 248 | * border is by setting it's border-style to `none` in the user-agent 249 | * stylesheet. 250 | * 251 | * In order to easily add borders to elements by just setting the `border-width` 252 | * property, we change the default border-style for all elements to `solid`, and 253 | * use border-width to hide them instead. This way our `border` utilities only 254 | * need to set the `border-width` property instead of the entire `border` 255 | * shorthand, making our border utilities much more straightforward to compose. 256 | * 257 | * https://github.com/tailwindcss/tailwindcss/pull/116 258 | */*, 259 | ::before, 260 | ::after { 261 | box-sizing: border-box; /* 1 */ 262 | border-width: 0; /* 2 */ 263 | border-style: solid; /* 2 */ 264 | border-color: #e5e7eb; /* 2 */ 265 | }/* 266 | * Ensure horizontal rules are visible by default 267 | */hr { 268 | border-top-width: 1px; 269 | }/** 270 | * Undo the `border-style: none` reset that Normalize applies to images so that 271 | * our `border-{width}` utilities have the expected effect. 272 | * 273 | * The Normalize reset is unnecessary for us since we default the border-width 274 | * to 0 on all elements. 275 | * 276 | * https://github.com/tailwindcss/tailwindcss/issues/362 277 | */img { 278 | border-style: solid; 279 | }textarea { 280 | resize: vertical; 281 | }input::placeholder, 282 | textarea::placeholder { 283 | opacity: 1; 284 | color: #9ca3af; 285 | }button, 286 | [role="button"] { 287 | cursor: pointer; 288 | }table { 289 | border-collapse: collapse; 290 | }h1, 291 | h2, 292 | h3, 293 | h4, 294 | h5, 295 | h6 { 296 | font-size: inherit; 297 | font-weight: inherit; 298 | }/** 299 | * Reset links to optimize for opt-in styling instead of 300 | * opt-out. 301 | */a { 302 | color: inherit; 303 | text-decoration: inherit; 304 | }/** 305 | * Reset form element properties that are easy to forget to 306 | * style explicitly so you don't inadvertently introduce 307 | * styles that deviate from your design system. These styles 308 | * supplement a partial reset that is already applied by 309 | * normalize.css. 310 | */button, 311 | input, 312 | optgroup, 313 | select, 314 | textarea { 315 | padding: 0; 316 | line-height: inherit; 317 | color: inherit; 318 | }/** 319 | * Use the configured 'mono' font family for elements that 320 | * are expected to be rendered with a monospace font, falling 321 | * back to the system monospace stack if there is no configured 322 | * 'mono' font family. 323 | */pre, 324 | code, 325 | kbd, 326 | samp { 327 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 328 | }/** 329 | * Make replaced elements `display: block` by default as that's 330 | * the behavior you want almost all of the time. Inspired by 331 | * CSS Remedy, with `svg` added as well. 332 | * 333 | * https://github.com/mozdevs/cssremedy/issues/14 334 | */img, 335 | svg, 336 | video, 337 | canvas, 338 | audio, 339 | iframe, 340 | embed, 341 | object { 342 | display: block; 343 | vertical-align: middle; 344 | }/** 345 | * Constrain images and videos to the parent width and preserve 346 | * their instrinsic aspect ratio. 347 | * 348 | * https://github.com/mozdevs/cssremedy/issues/14 349 | */img, 350 | video { 351 | max-width: 100%; 352 | height: auto; 353 | } 354 | * { 355 | --tw-shadow: 0 0 #0000 356 | } 357 | * { 358 | --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); 359 | --tw-ring-offset-width: 0px; 360 | --tw-ring-offset-color: #fff; 361 | --tw-ring-color: rgba(59, 130, 246, 0.5); 362 | --tw-ring-offset-shadow: 0 0 #0000; 363 | --tw-ring-shadow: 0 0 #0000 364 | } 365 | @keyframes spin { 366 | to { 367 | transform: rotate(360deg) 368 | } 369 | } 370 | @keyframes ping { 371 | 75%, 100% { 372 | transform: scale(2); 373 | opacity: 0 374 | } 375 | } 376 | @keyframes pulse { 377 | 50% { 378 | opacity: .5 379 | } 380 | } 381 | @keyframes bounce { 382 | 0%, 100% { 383 | transform: translateY(-25%); 384 | animation-timing-function: cubic-bezier(0.8,0,1,1) 385 | } 386 | 50% { 387 | transform: none; 388 | animation-timing-function: cubic-bezier(0,0,0.2,1) 389 | } 390 | } 391 | .border-primary-800 { 392 | --tw-border-opacity: 1; 393 | border-color: rgba(32, 73, 89, var(--tw-border-opacity)) 394 | } 395 | .border-b { 396 | border-bottom-width: 1px 397 | } 398 | .flex { 399 | display: flex 400 | } 401 | .flex-col { 402 | flex-direction: column 403 | } 404 | .items-center { 405 | align-items: center 406 | } 407 | .justify-center { 408 | justify-content: center 409 | } 410 | .h-12 { 411 | height: 3rem 412 | } 413 | .h-48 { 414 | height: 12rem 415 | } 416 | .h-full { 417 | height: 100% 418 | } 419 | .h-screen { 420 | height: 100vh 421 | } 422 | .ml-5 { 423 | margin-left: 1.25rem 424 | } 425 | .pb-0 { 426 | padding-bottom: 0px 427 | } 428 | .pt-12 { 429 | padding-top: 3rem 430 | } 431 | .fixed { 432 | position: fixed 433 | } 434 | .top-0 { 435 | top: 0px 436 | } 437 | .shadow-xl { 438 | --tw-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 439 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow) 440 | } 441 | .w-screen { 442 | width: 100vw 443 | } 444 | .z-0 { 445 | z-index: 0 446 | } -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | import type { BrowserWindowConstructorOptions } from 'electron' 3 | import contextMenu from 'electron-context-menu' 4 | import windowStateKeeper from 'electron-window-state' 5 | import { getTwConfig, getTwConfigPath } from '@twstyled/util' 6 | 7 | const resolvedTailwindConfig = getTwConfig(getTwConfigPath()) 8 | 9 | const isDevelopment = !app.isPackaged 10 | 11 | function createWindow() { 12 | const windowOptions: BrowserWindowConstructorOptions = { 13 | minWidth: 800, 14 | minHeight: 600, 15 | backgroundColor: resolvedTailwindConfig.theme.colors.primary[800], 16 | titleBarStyle: 'hidden', 17 | autoHideMenuBar: true, 18 | trafficLightPosition: { 19 | x: 20, 20 | y: 32 21 | }, 22 | webPreferences: { 23 | contextIsolation: true, 24 | devTools: isDevelopment, 25 | spellcheck: false, 26 | nodeIntegration: true 27 | }, 28 | show: false 29 | } 30 | 31 | contextMenu({ 32 | showSearchWithGoogle: false, 33 | showCopyImage: false, 34 | prepend: (defaultActions, params, browserWindow) => [ 35 | { 36 | label: 'its like magic 💥' 37 | } 38 | ] 39 | }) 40 | 41 | const windowState = windowStateKeeper({ 42 | defaultWidth: windowOptions.minWidth, 43 | defaultHeight: windowOptions.minHeight 44 | }) 45 | 46 | const browserWindow = new BrowserWindow({ 47 | ...windowOptions, 48 | x: windowState.x, 49 | y: windowState.y, 50 | width: windowState.width, 51 | height: windowState.height 52 | }) 53 | 54 | windowState.manage(browserWindow) 55 | 56 | browserWindow.once('ready-to-show', () => { 57 | browserWindow.show() 58 | browserWindow.focus() 59 | }) 60 | 61 | const port = process.env.PORT || 3000 62 | 63 | if (isDevelopment) { 64 | void browserWindow.loadURL(`http://localhost:${port}`) 65 | } else { 66 | void browserWindow.loadFile('./index.html') 67 | } 68 | } 69 | 70 | void app.whenReady().then(createWindow) 71 | 72 | app.on('window-all-closed', () => { 73 | app.quit() 74 | }) 75 | 76 | app.on('activate', () => { 77 | if (BrowserWindow.getAllWindows().length === 0) { 78 | createWindow() 79 | } 80 | }) 81 | -------------------------------------------------------------------------------- /src/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2018", 5 | "module": "CommonJS", 6 | "sourceMap": false, 7 | "outDir": "../../dist", 8 | "baseUrl": "../../src", 9 | "paths": { 10 | "@/main/*": ["main/*"], 11 | "@/common/*": ["common/*"] 12 | }, 13 | }, 14 | "include": ["../main/**/*", "../common/**/*"], 15 | "exclude": ["../renderer/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from 'react' 2 | import { motion } from 'framer-motion' 3 | import TopBar from './components/top-bar' 4 | import logo from './logo.png' 5 | 6 | const containerMotion = { 7 | initial: 'hidden', 8 | animate: 'visible', 9 | variants: { 10 | hidden: { opacity: 1, scale: 0 }, 11 | visible: { 12 | opacity: 1, 13 | scale: 1, 14 | transition: { 15 | delayChildren: 0.3, 16 | staggerChildren: 0.2 17 | } 18 | } 19 | } 20 | } 21 | 22 | function App() { 23 | return ( 24 |
25 | 26 | 27 |
28 | 29 |
30 |
31 |
32 | ) 33 | } 34 | 35 | export default App 36 | -------------------------------------------------------------------------------- /src/renderer/components/top-bar.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@twstyled/core' 2 | 3 | const TopBar = styled.nav` 4 | -webkit-app-region: drag; 5 | @tailwind w-screen h-12 border-primary-800 border-b items-center flex shadow-xl z-0 fixed top-0; 6 | ` 7 | 8 | export default TopBar 9 | -------------------------------------------------------------------------------- /src/renderer/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/renderer/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 3 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 4 | sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | 8 | /* used for behaving more like an app, selection inside inputs and textareas still work in chromium */ 9 | user-select: none; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 14 | monospace; 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/renderer/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twstyled/electron-vite-react/045bf9642e8e2777c32c62bf937078c7eb061963/src/renderer/logo.png -------------------------------------------------------------------------------- /src/renderer/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/renderer/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import '../index.twstyled.css' 4 | import './index.css' 5 | import App from './App' 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ) 13 | -------------------------------------------------------------------------------- /src/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "types": ["vite/client", "@twstyled/core"], 6 | "target": "esnext", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "importHelpers": true, 10 | "jsx": "preserve", 11 | "esModuleInterop": true, 12 | "sourceMap": true, 13 | "baseUrl": "../../src", 14 | "paths": { 15 | "@/renderer/*": ["renderer/*"], 16 | "@/common/*": ["common/*"] 17 | }, 18 | "allowSyntheticDefaultImports": true 19 | }, 20 | "include": ["../renderer/**/*", "../common/**/*"], 21 | "exclude": ["../main/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: false, 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | fontFamily: { 6 | // caption uses the systemfont so it looks more native 7 | display: ['caption'], 8 | body: ['caption'], 9 | }, 10 | extend: { 11 | colors: { 12 | primary: { 13 | 100: '#91C4D7', 14 | 200: '#65ACC8', 15 | 300: '#4FA0C0', 16 | 400: '#4091B1', 17 | 500: '#387F9B', 18 | 600: '#306D85', 19 | 700: '#285B6F', 20 | 800: '#204959', 21 | 900: '#183642', 22 | }, 23 | }, 24 | }, 25 | }, 26 | variants: { 27 | outline: ['focus', 'hover'], 28 | border: ['focus', 'hover'], 29 | }, 30 | plugins: [], 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "isolatedModules": true, 7 | "jsx": "preserve", 8 | "lib": ["es6"], 9 | "moduleResolution": "node", 10 | "module": "commonjs", 11 | "forceConsistentCasingInFileNames": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "noImplicitAny": true, 15 | "strict": true, 16 | "noUnusedLocals": false, 17 | "pretty": true, 18 | "skipLibCheck": true, 19 | "target": "ES6", 20 | "resolveJsonModule": true, 21 | "declaration": true, 22 | "sourceMap": true, 23 | "baseUrl": ".", 24 | "paths": { 25 | "@/renderer/*": ["src/renderer/*"], 26 | "@/common/*": ["src/common/*"] 27 | } 28 | }, 29 | "include": [ 30 | "**/*.ts", 31 | "**/*.tsx" 32 | ], 33 | "exclude": ["node_modules"] 34 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | import reactRefresh from '@vitejs/plugin-react-refresh' 4 | import twstyled from 'vite-plugin-twstyled' 5 | 6 | export default defineConfig({ 7 | plugins: [twstyled(), reactRefresh()], 8 | base: './', 9 | root: resolve('./src/renderer'), 10 | build: { 11 | outDir: resolve('./dist'), 12 | emptyOutDir: true 13 | }, 14 | resolve: { 15 | alias: [ 16 | { 17 | find: '@/renderer', 18 | replacement: resolve(__dirname, 'src/renderer') 19 | }, 20 | { 21 | find: '@/common', 22 | replacement: resolve(__dirname, 'src/common') 23 | } 24 | ] 25 | } 26 | }) 27 | --------------------------------------------------------------------------------