Icedsoda committed 2025-02-18 15:38:00 +01:00
1 parent dcab796294
commit 531d5d0d9d
99 files changed
+19023 -1624

No files matched your search

-9
View File
@@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
+42
View File
@@ -0,0 +1,42 @@
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
tests-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
test-e2e:
timeout-minutes: 10
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Create Executable
run: npm run make
- name: Run Playwright tests
run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
+99 -4
View File
@@ -1,6 +1,101 @@
node_modules
dist
out
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
*.log*
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Webpack
.webpack/
# Vite
.vite/
dist/
# Electron-Forge
out/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
-1
View File
@@ -1 +0,0 @@
shamefully-hoist=true
+5 -6
View File
@@ -1,6 +1,5 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json
# Add files here to ignore them from prettier formatting
/dist
/coverage
/.vite
README.md
+8
View File
@@ -0,0 +1,8 @@
{
"singleQuote": false,
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"]
}
-4
View File
@@ -1,4 +0,0 @@
singleQuote: true
semi: false
printWidth: 100
trailingComma: none
-3
View File
@@ -1,3 +0,0 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}
-39
View File
@@ -1,39 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}
-11
View File
@@ -1,11 +0,0 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Luan Roger
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+132 -19
View File
@@ -1,34 +1,147 @@
# electron-app
# electron-shadcn
An Electron application with React and TypeScript
Electron in all its glory. Everything you will need to develop your beautiful desktop application.
## Recommended IDE Setup
![Demo GIF](https://github.com/LuanRoger/electron-shadcn/blob/main/images/demo.gif)
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
## Libs and tools
## Project Setup
To develop a Electron app, you probably will need some UI, test, formatter, style or other kind of library or framework, so let me install and configure some of them to you.
### Install
### Core 🏍️
```bash
$ pnpm install
- [Electron 34](https://www.electronjs.org)
- [Vite 6](https://vitejs.dev)
- [SWC](https://swc.rs)
### DX 🛠️
- [TypeScript 5.7](https://www.typescriptlang.org)
- [Prettier](https://prettier.io)
- [ESLint 9](https://eslint.org)
- [Zod](https://zod.dev)
- [React Query (TanStack)](https://react-query.tanstack.com)
### UI 🎨
- [React 19](https://reactjs.org)
- [Tailwind 4](https://tailwindcss.com)
- [Shadcn UI](https://ui.shadcn.com)
- [Geist](https://vercel.com/font) as default font
- [i18next](https://www.i18next.com)
- [TanStack Router](https://tanstack.com/router)
- [Lucide](https://lucide.dev)
### Test 🧪
- [Vitest](https://vitest.dev)
- [Playwright](https://playwright.dev)
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)
### Packing and distribution 📦
- [Electron Forge](https://www.electronforge.io)
### CI/CD 🚀
- Pre-configured [GitHub Actions workflow](https://github.com/LuanRoger/electron-shadcn/blob/main/.github/workflows/playwright.yml), for test with Playwright
### Project preferences 🎯
- Use Context isolation
- [React Compiler](https://react.dev/learn/react-compiler) is enabled by default.
- `titleBarStyle`: hidden (Using custom title bar)
- Geist as default font
- Some default styles was applied, check the [`styles`](https://github.com/LuanRoger/electron-shadcn/tree/main/src/styles) directory
- React DevTools are installed by default
> If you don't know some of these libraries or tools, I recommend you to check their documentation to understand how they work and how to use them.
## Directory structure
```plaintext
.
└── ./src/
├── ./src/assets/
│ └── ./src/assets/fonts/
├── ./src/components/
│ ├── ./src/components/template
│ └── ./src/components/ui/
├── ./src/helpers/
│ └── ./src/helpers/ipc/
├── ./src/layout/
├── ./src/lib/
├── ./src/pages/
├── ./src/style/
└── ./src/tests/
```
### Development
- `src/`: Main directory
- `assets/`: Store assets like images, fonts, etc.
- `components/`: Store UI components
- `template/`: Store the all not important components used by the template. It doesn't include the `WindowRegion` or the theme toggler, if you want to start an empty project, you can safely delete this directory.
- `ui/`: Store Shadcn UI components (this is the default direcotry used by Shadcn UI)
- `helpers/`: Store IPC related functions to be called in the renderer process
- `ipc/`: Directory to store IPC context and listener functions
- Some implementations are already done, like `theme` and `window` for the custom title bar
- `layout/`: Directory to store layout components
- `lib/`: Store libraries and other utilities
- `pages/`: Store app's pages
- `style/`: Store global styles
- `tests/`: Store tests (from Vitest and Playwright)
## NPM script
To run any of those scripts:
```bash
$ pnpm dev
npm run <script>
```
### Build
- `start`: Start the app in development mode
- `package`: Package your application into a platform-specific executable bundle and put the result in a folder.
- `make`: Generate platform-specific distributables (e.g. .exe, .dmg, etc) of your application for distribution.
- `publish`: Electron Forge's 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.
- `lint`: Run ESLint to lint the code
- `format`: Run Prettier to check the code (it doesn't change the code)
- `format:write`: Run Prettier to format the code
- `test`: Run the default unit-test script (Vitest)
- `test:watch`: Run the default unit-test script in watch mode (Vitest)
- `test:unit`: Run the Vitest tests
- `test:e2e`: Run the Playwright tests
- `test:all`: Run all tests (Vitest and Playwright)
> The test scripts involving Playwright require the app be builded before running the tests. So, before run the tests, run the `package`, `make` or `publish` script.
## How to use
1. Clone this repository
```bash
# For windows
$ pnpm build:win
# For macOS
$ pnpm build:mac
# For Linux
$ pnpm build:linux
git clone https://github.com/LuanRoger/electron-shadcn.git
```
Or use it as a template on GitHub
2. Install dependencies
```bash
npm install
```
3. Run the app
```bash
npm run start
```
## Used by
- [yaste](https://github.com/LuanRoger/yaste) - yaste (Yet another super ₛᵢₘₚₗₑ text editor) is a text editor, that can be used as an alternative to the native text editor of your SO, maybe.
- [eletric-drizzle](https://github.com/LuanRoger/electric-drizzle) - shadcn-ui and Drizzle ORM with Electron.
> Does you've used this template in your project? Add it here and open a PR.
## License
This project is licensed under the MIT License - see the [LICENSE](https://github.com/LuanRoger/electron-shadcn/blob/main/LICENSE) file for details.
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

+17
View File
@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "@/styles/global.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
-43
View File
@@ -1,43 +0,0 @@
appId: com.electron.app
productName: electron-app
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
asarUnpack:
- resources/**
win:
executableName: electron-app
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
-20
View File
@@ -1,20 +0,0 @@
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()]
},
preload: {
plugins: [externalizeDepsPlugin()]
},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src')
}
},
plugins: [react()]
}
})
+26 -25
View File
@@ -1,31 +1,32 @@
import tseslint from '@electron-toolkit/eslint-config-ts'
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier'
import eslintPluginReact from 'eslint-plugin-react'
import eslintPluginReactHooks from 'eslint-plugin-react-hooks'
import eslintPluginReactRefresh from 'eslint-plugin-react-refresh'
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import eslintPluginPrettierRecommended from "eslint-config-prettier";
import reactCompiler from "eslint-plugin-react-compiler";
import path from "node:path";
import { includeIgnoreFile } from "@eslint/compat";
import { fileURLToPath } from "node:url";
export default tseslint.config(
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
tseslint.configs.recommended,
eslintPluginReact.configs.flat.recommended,
eslintPluginReact.configs.flat['jsx-runtime'],
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const prettierIgnorePath = path.resolve(__dirname, ".prettierignore");
/** @type {import('eslint').Linter.Config[]} */
export default [
includeIgnoreFile(prettierIgnorePath),
{
settings: {
react: {
version: 'detect'
}
}
},
{
files: ['**/*.{ts,tsx}'],
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
plugins: {
'react-hooks': eslintPluginReactHooks,
'react-refresh': eslintPluginReactRefresh
"react-compiler": reactCompiler,
},
rules: {
...eslintPluginReactHooks.configs.recommended.rules,
...eslintPluginReactRefresh.configs.vite.rules
}
"react-compiler/react-compiler": "error",
},
},
eslintConfigPrettier
)
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
pluginReact.configs.flat.recommended,
eslintPluginPrettierRecommended,
...tseslint.configs.recommended,
];
+58
View File
@@ -0,0 +1,58 @@
import type { ForgeConfig } from "@electron-forge/shared-types";
import { MakerSquirrel } from "@electron-forge/maker-squirrel";
import { MakerZIP } from "@electron-forge/maker-zip";
import { MakerDeb } from "@electron-forge/maker-deb";
import { MakerRpm } from "@electron-forge/maker-rpm";
import { VitePlugin } from "@electron-forge/plugin-vite";
import { FusesPlugin } from "@electron-forge/plugin-fuses";
import { FuseV1Options, FuseVersion } from "@electron/fuses";
const config: ForgeConfig = {
packagerConfig: {
executableName: "electron-shadcn",
asar: true,
},
rebuildConfig: {},
makers: [
new MakerSquirrel({}),
new MakerZIP({}, ["darwin"]),
new MakerRpm({}),
new MakerDeb({}),
],
plugins: [
new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
// If you are familiar with Vite configuration, it will look really familiar.
build: [
{
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
entry: "src/main.ts",
config: "vite.main.config.ts",
},
{
entry: "src/preload.ts",
config: "vite.preload.config.ts",
},
],
renderer: [
{
name: "main_window",
config: "vite.renderer.config.ts",
},
],
}),
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};
export default config;
+35
View File
@@ -0,0 +1,35 @@
export {}; // Make this a module
declare global {
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite
// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on
// whether you're running in development or production).
const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
const MAIN_WINDOW_VITE_NAME: string;
namespace NodeJS {
interface Process {
// Used for hot reload after preload scripts.
viteDevServers: Record<string, import("vite").ViteDevServer>;
}
}
type VitePluginConfig = ConstructorParameters<
typeof import("@electron-forge/plugin-vite").VitePlugin
>[0];
interface VitePluginRuntimeKeys {
VITE_DEV_SERVER_URL: `${string}_VITE_DEV_SERVER_URL`;
VITE_NAME: `${string}_VITE_NAME`;
}
}
declare module "vite" {
interface ConfigEnv<
K extends keyof VitePluginConfig = keyof VitePluginConfig,
> {
root: string;
forgeConfig: VitePluginConfig;
forgeConfigSelf: VitePluginConfig[K][number];
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 886 KiB

+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>electron-shadcn Template</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self';" />
<link rel="stylesheet" href="/src/styles/global.css" />
</head>
<body>
<div id="app" />
<script type="module" src="/src/renderer.ts"></script>
</body>
</html>
+13024
View File
File diff suppressed because it is too large. Load diff
+82 -40
View File
@@ -1,48 +1,90 @@
{
"name": "electron-app",
"name": "electron-shadcn",
"productName": "electron-shadcn Template",
"version": "1.0.0",
"description": "An Electron application with React and TypeScript",
"main": "./out/main/index.js",
"author": "example.com",
"homepage": "https://electron-vite.org",
"description": "Electron Forge with shadcn-ui (Vite + Typescript)",
"main": ".vite/build/main.js",
"private": true,
"scripts": {
"format": "prettier --write .",
"lint": "eslint --cache .",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux"
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "eslint .",
"format": "prettier --check .",
"format:write": "prettier --write .",
"test": "vitest run",
"test:watch": "vitest watch",
"test:unit": "vitest",
"test:e2e": "playwright test",
"test:all": "vitest run && playwright test"
},
"author": "ROG <luan.roger.2003@gmail.com>",
"license": "MIT",
"devDependencies": {
"@electron-forge/cli": "^7.6.1",
"@electron-forge/maker-deb": "^7.6.1",
"@electron-forge/maker-rpm": "^7.6.1",
"@electron-forge/maker-squirrel": "^7.6.1",
"@electron-forge/maker-zip": "^7.6.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
"@electron-forge/plugin-fuses": "^7.6.1",
"@electron-forge/plugin-vite": "^7.6.1",
"@electron-forge/shared-types": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4.0.6",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/electron-squirrel-startup": "^1.0.2",
"@types/eslint-config-prettier": "^6.11.3",
"@types/node": "^22.13.4",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"electron": "34.0.2",
"electron-devtools-installer": "^4.0.0",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
"globals": "^15.15.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.2",
"prettier": "^3.5.1",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.1.0",
"vitest": "^3.0.6"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^4.0.0"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@types/node": "^22.13.4",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@icons-pack/react-simple-icons": "^11.2.0",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.2",
"@tanstack/react-query": "^5.66.7",
"@tanstack/react-router": "^1.105.0",
"@tanstack/router-devtools": "^1.105.0",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^34.2.0",
"electron-builder": "^25.1.8",
"electron-vite": "^3.0.0",
"eslint": "^9.20.1",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"prettier": "^3.5.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.7.3",
"vite": "^6.1.0"
"babel-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"electron-squirrel-startup": "^1.0.1",
"i18next": "^24.2.2",
"lucide-react": "^0.475.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-i18next": "^15.4.1",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.2"
}
}
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig, devices } from "@playwright/test";
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./src/tests/e2e",
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});
+3912 -903
View File
File diff suppressed because it is too large. Load diff
+5
View File
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

+26
View File
@@ -0,0 +1,26 @@
import React, { useEffect } from "react";
import { createRoot } from "react-dom/client";
import { syncThemeWithLocal } from "./helpers/theme_helpers";
import { useTranslation } from "react-i18next";
import "./localization/i18n";
import { updateAppLanguage } from "./helpers/language_helpers";
import { router } from "./routes/router";
import { RouterProvider } from "@tanstack/react-router";
export default function App() {
const { i18n } = useTranslation();
useEffect(() => {
syncThemeWithLocal();
updateAppLanguage(i18n);
}, [i18n]);
return <RouterProvider router={router} />;
}
const root = createRoot(document.getElementById("app")!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,91 @@
import {
closeWindow,
maximizeWindow,
minimizeWindow,
} from "@/helpers/window_helpers";
import React, { type ReactNode } from "react";
interface DragWindowRegionProps {
title?: ReactNode;
}
export default function DragWindowRegion({ title }: DragWindowRegionProps) {
return (
<div className="flex w-screen items-stretch justify-between">
<div className="draglayer w-full">
{title && (
<div className="flex flex-1 select-none whitespace-nowrap p-2 text-xs text-gray-400">
{title}
</div>
)}
</div>
<WindowButtons />
</div>
);
}
function WindowButtons() {
return (
<div className="flex">
<button
title="Minimize"
type="button"
className="p-2 hover:bg-slate-300"
onClick={minimizeWindow}
>
<svg
aria-hidden="true"
role="img"
width="12"
height="12"
viewBox="0 0 12 12"
>
<rect fill="currentColor" width="10" height="1" x="1" y="6"></rect>
</svg>
</button>
<button
title="Maximize"
type="button"
className="p-2 hover:bg-slate-300"
onClick={maximizeWindow}
>
<svg
aria-hidden="true"
role="img"
width="12"
height="12"
viewBox="0 0 12 12"
>
<rect
width="9"
height="9"
x="1.5"
y="1.5"
fill="none"
stroke="currentColor"
></rect>
</svg>
</button>
<button
type="button"
title="Close"
className="p-2 hover:bg-red-300"
onClick={closeWindow}
>
<svg
aria-hidden="true"
role="img"
width="12"
height="12"
viewBox="0 0 12 12"
>
<polygon
fill="currentColor"
fillRule="evenodd"
points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"
></polygon>
</svg>
</button>
</div>
);
}
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
import langs from "@/localization/langs";
import { useTranslation } from "react-i18next";
import { setAppLanguage } from "@/helpers/language_helpers";
export default function LangToggle() {
const { i18n } = useTranslation();
const currentLang = i18n.language;
function onValueChange(value: string) {
setAppLanguage(value, i18n);
}
return (
<ToggleGroup
type="single"
onValueChange={onValueChange}
value={currentLang}
>
{langs.map((lang) => (
<ToggleGroupItem key={lang.key} value={lang.key}>
{`${lang.prefix} ${lang.nativeName}`}
</ToggleGroupItem>
))}
</ToggleGroup>
);
}
+12
View File
@@ -0,0 +1,12 @@
import { Moon } from "lucide-react";
import React from "react";
import { Button } from "@/components/ui/button";
import { toggleTheme } from "@/helpers/theme_helpers";
export default function ToggleTheme() {
return (
<Button onClick={toggleTheme} size="icon">
<Moon size={16} />
</Button>
);
}
@@ -0,0 +1,10 @@
import React from "react";
export default function Footer() {
return (
<footer className="font-tomorrow inline-flex justify-between text-[0.7rem] uppercase text-muted-foreground">
<p>Made by LuanRoger - Based in Brazil 🇧🇷</p>
<p>Powered by Electron</p>
</footer>
);
}
@@ -0,0 +1,14 @@
import React from "react";
import { SiElectron, SiReact, SiVite } from "@icons-pack/react-simple-icons";
export default function InitalIcons() {
const iconSize = 48;
return (
<div className="inline-flex gap-2">
<SiReact size={iconSize} />
<SiVite size={iconSize} />
<SiElectron size={iconSize} />
</div>
);
}
@@ -0,0 +1,35 @@
import React from "react";
import { Link } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import {
NavigationMenu as NavigationMenuBase,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
navigationMenuTriggerStyle,
} from "../ui/navigation-menu";
export default function NavigationMenu() {
const { t } = useTranslation();
return (
<NavigationMenuBase className="px-2 font-mono text-muted-foreground">
<NavigationMenuList>
<NavigationMenuItem>
<Link to="/">
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
{t("titleHomePage")}
</NavigationMenuLink>
</Link>
</NavigationMenuItem>
<NavigationMenuItem>
<Link to="/second-page">
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
{t("titleSecondPage")}
</NavigationMenuLink>
</Link>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenuBase>
);
}
+56
View File
@@ -0,0 +1,56 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/tailwind";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background cursor-pointer transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
@@ -0,0 +1,128 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/utils/tailwind"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}
@@ -0,0 +1,59 @@
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { VariantProps } from "class-variance-authority";
import { cn } from "@/utils/tailwind";
import { toggleVariants } from "@/components/ui/toggle";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };
+43
View File
@@ -0,0 +1,43 @@
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/tailwind";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background cursor-pointer transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };
@@ -0,0 +1,7 @@
import { exposeThemeContext } from "./theme/theme-context";
import { exposeWindowContext } from "./window/window-context";
export default function exposeContexts() {
exposeWindowContext();
exposeThemeContext();
}
@@ -0,0 +1,8 @@
import { BrowserWindow } from "electron";
import { addThemeEventListeners } from "./theme/theme-listeners";
import { addWindowEventListeners } from "./window/window-listeners";
export default function registerListeners(mainWindow: BrowserWindow) {
addWindowEventListeners(mainWindow);
addThemeEventListeners();
}
@@ -0,0 +1,5 @@
export const THEME_MODE_CURRENT_CHANNEL = "theme-mode:current";
export const THEME_MODE_TOGGLE_CHANNEL = "theme-mode:toggle";
export const THEME_MODE_DARK_CHANNEL = "theme-mode:dark";
export const THEME_MODE_LIGHT_CHANNEL = "theme-mode:light";
export const THEME_MODE_SYSTEM_CHANNEL = "theme-mode:system";
@@ -0,0 +1,18 @@
import {
THEME_MODE_CURRENT_CHANNEL,
THEME_MODE_DARK_CHANNEL,
THEME_MODE_LIGHT_CHANNEL,
THEME_MODE_SYSTEM_CHANNEL,
THEME_MODE_TOGGLE_CHANNEL,
} from "./theme-channels";
export function exposeThemeContext() {
const { contextBridge, ipcRenderer } = window.require("electron");
contextBridge.exposeInMainWorld("themeMode", {
current: () => ipcRenderer.invoke(THEME_MODE_CURRENT_CHANNEL),
toggle: () => ipcRenderer.invoke(THEME_MODE_TOGGLE_CHANNEL),
dark: () => ipcRenderer.invoke(THEME_MODE_DARK_CHANNEL),
light: () => ipcRenderer.invoke(THEME_MODE_LIGHT_CHANNEL),
system: () => ipcRenderer.invoke(THEME_MODE_SYSTEM_CHANNEL),
});
}
@@ -0,0 +1,33 @@
import { nativeTheme } from "electron";
import { ipcMain } from "electron";
import {
THEME_MODE_CURRENT_CHANNEL,
THEME_MODE_DARK_CHANNEL,
THEME_MODE_LIGHT_CHANNEL,
THEME_MODE_SYSTEM_CHANNEL,
THEME_MODE_TOGGLE_CHANNEL,
} from "./theme-channels";
export function addThemeEventListeners() {
ipcMain.handle(THEME_MODE_CURRENT_CHANNEL, () => nativeTheme.themeSource);
ipcMain.handle(THEME_MODE_TOGGLE_CHANNEL, () => {
if (nativeTheme.shouldUseDarkColors) {
nativeTheme.themeSource = "light";
} else {
nativeTheme.themeSource = "dark";
}
return nativeTheme.shouldUseDarkColors;
});
ipcMain.handle(
THEME_MODE_DARK_CHANNEL,
() => (nativeTheme.themeSource = "dark"),
);
ipcMain.handle(
THEME_MODE_LIGHT_CHANNEL,
() => (nativeTheme.themeSource = "light"),
);
ipcMain.handle(THEME_MODE_SYSTEM_CHANNEL, () => {
nativeTheme.themeSource = "system";
return nativeTheme.shouldUseDarkColors;
});
}
@@ -0,0 +1,3 @@
export const WIN_MINIMIZE_CHANNEL = "window:minimize";
export const WIN_MAXIMIZE_CHANNEL = "window:maximize";
export const WIN_CLOSE_CHANNEL = "window:close";
@@ -0,0 +1,14 @@
import {
WIN_MINIMIZE_CHANNEL,
WIN_MAXIMIZE_CHANNEL,
WIN_CLOSE_CHANNEL,
} from "./window-channels";
export function exposeWindowContext() {
const { contextBridge, ipcRenderer } = window.require("electron");
contextBridge.exposeInMainWorld("electronWindow", {
minimize: () => ipcRenderer.invoke(WIN_MINIMIZE_CHANNEL),
maximize: () => ipcRenderer.invoke(WIN_MAXIMIZE_CHANNEL),
close: () => ipcRenderer.invoke(WIN_CLOSE_CHANNEL),
});
}
@@ -0,0 +1,22 @@
import { BrowserWindow, ipcMain } from "electron";
import {
WIN_CLOSE_CHANNEL,
WIN_MAXIMIZE_CHANNEL,
WIN_MINIMIZE_CHANNEL,
} from "./window-channels";
export function addWindowEventListeners(mainWindow: BrowserWindow) {
ipcMain.handle(WIN_MINIMIZE_CHANNEL, () => {
mainWindow.minimize();
});
ipcMain.handle(WIN_MAXIMIZE_CHANNEL, () => {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
});
ipcMain.handle(WIN_CLOSE_CHANNEL, () => {
mainWindow.close();
});
}
+19
View File
@@ -0,0 +1,19 @@
import type { i18n } from "i18next";
const languageLocalStorageKey = "lang";
export function setAppLanguage(lang: string, i18n: i18n) {
localStorage.setItem(languageLocalStorageKey, lang);
i18n.changeLanguage(lang);
document.documentElement.lang = lang;
}
export function updateAppLanguage(i18n: i18n) {
const localLang = localStorage.getItem(languageLocalStorageKey);
if (!localLang) {
return;
}
i18n.changeLanguage(localLang);
document.documentElement.lang = localLang;
}
+64
View File
@@ -0,0 +1,64 @@
import { ThemeMode } from "@/types/theme-mode";
const THEME_KEY = "theme";
export interface ThemePreferences {
system: ThemeMode;
local: ThemeMode | null;
}
export async function getCurrentTheme(): Promise<ThemePreferences> {
const currentTheme = await window.themeMode.current();
const localTheme = localStorage.getItem(THEME_KEY) as ThemeMode | null;
return {
system: currentTheme,
local: localTheme,
};
}
export async function setTheme(newTheme: ThemeMode) {
switch (newTheme) {
case "dark":
await window.themeMode.dark();
updateDocumentTheme(true);
break;
case "light":
await window.themeMode.light();
updateDocumentTheme(false);
break;
case "system": {
const isDarkMode = await window.themeMode.system();
updateDocumentTheme(isDarkMode);
break;
}
}
localStorage.setItem(THEME_KEY, newTheme);
}
export async function toggleTheme() {
const isDarkMode = await window.themeMode.toggle();
const newTheme = isDarkMode ? "dark" : "light";
updateDocumentTheme(isDarkMode);
localStorage.setItem(THEME_KEY, newTheme);
}
export async function syncThemeWithLocal() {
const { local } = await getCurrentTheme();
if (!local) {
setTheme("system");
return;
}
await setTheme(local);
}
function updateDocumentTheme(isDarkMode: boolean) {
if (!isDarkMode) {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
}
+9
View File
@@ -0,0 +1,9 @@
export async function minimizeWindow() {
await window.electronWindow.minimize();
}
export async function maximizeWindow() {
await window.electronWindow.maximize();
}
export async function closeWindow() {
await window.electronWindow.close();
}
+17
View File
@@ -0,0 +1,17 @@
import React from "react";
import DragWindowRegion from "@/components/DragWindowRegion";
import NavigationMenu from "@/components/template/NavigationMenu";
export default function BaseLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<DragWindowRegion title="electron-shadcn" />
<NavigationMenu />
<main className="h-screen pb-20 p-2">{children}</main>
</>
);
}
+22
View File
@@ -0,0 +1,22 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
i18n.use(initReactI18next).init({
fallbackLng: "en",
resources: {
en: {
translation: {
appName: "electron-shadcn",
titleHomePage: "Home Page",
titleSecondPage: "Second Page",
},
},
"pt-BR": {
translation: {
appName: "electron-shadcn",
titleHomePage: "Página Inicial",
titleSecondPage: "Segunda Página",
},
},
},
});
+14
View File
@@ -0,0 +1,14 @@
import { Language } from "./language";
export default [
{
key: "en",
nativeName: "English",
prefix: "🇺🇸",
},
{
key: "pt-BR",
nativeName: "Português (Brasil)",
prefix: "🇧🇷",
},
] satisfies Language[];
+5
View File
@@ -0,0 +1,5 @@
export interface Language {
key: string;
nativeName: string;
prefix: string;
}
+62
View File
@@ -0,0 +1,62 @@
import { app, BrowserWindow } from "electron";
import registerListeners from "./helpers/ipc/listeners-register";
// "electron-squirrel-startup" seems broken when packaging with vite
//import started from "electron-squirrel-startup";
import path from "path";
import {
installExtension,
REACT_DEVELOPER_TOOLS,
} from "electron-devtools-installer";
const inDevelopment = process.env.NODE_ENV === "development";
function createWindow() {
const preload = path.join(__dirname, "preload.js");
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
devTools: inDevelopment,
contextIsolation: true,
nodeIntegration: true,
nodeIntegrationInSubFrames: false,
preload: preload,
},
titleBarStyle: "hidden",
});
registerListeners(mainWindow);
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
);
}
}
async function installExtensions() {
try {
const result = await installExtension(REACT_DEVELOPER_TOOLS);
console.log(`Extensions installed successfully: ${result.name}`);
} catch {
console.error("Failed to install extensions");
}
}
app.whenReady().then(createWindow).then(installExtensions);
//osX only
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
//osX only ends
-74
View File
@@ -1,74 +0,0 @@
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
// IPC test
ipcMain.on('ping', () => console.log('pong'))
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
+27
View File
@@ -0,0 +1,27 @@
import React from "react";
import ToggleTheme from "@/components/ToggleTheme";
import { useTranslation } from "react-i18next";
import LangToggle from "@/components/LangToggle";
import Footer from "@/components/template/Footer";
import InitialIcons from "@/components/template/InitialIcons";
export default function HomePage() {
const { t } = useTranslation();
return (
<div className="flex h-full flex-col">
<div className="flex flex-1 flex-col items-center justify-center gap-2">
<InitialIcons />
<span>
<h1 className="font-mono text-4xl font-bold">{t("appName")}</h1>
<p className="text-end text-sm uppercase text-muted-foreground" data-testid="pageTitle">
{t("titleHomePage")}
</p>
</span>
<LangToggle />
<ToggleTheme />
</div>
<Footer />
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
import React from "react";
import Footer from "@/components/template/Footer";
import { useTranslation } from "react-i18next";
export default function SecondPage() {
const { t } = useTranslation();
return (
<div className="flex h-full flex-col">
<div className="flex flex-1 flex-col items-center justify-center gap-2">
<h1 className="text-4xl font-bold">{t("titleSecondPage")}</h1>
</div>
<Footer />
</div>
);
}
+3
View File
@@ -0,0 +1,3 @@
import exposeContexts from "./helpers/ipc/context-exposer";
exposeContexts();
-8
View File
@@ -1,8 +0,0 @@
import { ElectronAPI } from '@electron-toolkit/preload'
declare global {
interface Window {
electron: ElectronAPI
api: unknown
}
}
-22
View File
@@ -1,22 +0,0 @@
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
}
+1
View File
@@ -0,0 +1 @@
import "@/App";
-17
View File
@@ -1,17 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-35
View File
@@ -1,35 +0,0 @@
import Versions from './components/Versions'
import electronLogo from './assets/electron.svg'
function App(): JSX.Element {
const ipcHandle = (): void => window.electron.ipcRenderer.send('ping')
return (
<>
<img alt="logo" className="logo" src={electronLogo} />
<div className="creator">Powered by electron-vite</div>
<div className="text">
Build an Electron app with <span className="react">React</span>
&nbsp;and <span className="ts">TypeScript</span>
</div>
<p className="tip">
Please try pressing <code>F12</code> to open the devTool
</p>
<div className="actions">
<div className="action">
<a href="https://electron-vite.org/" target="_blank" rel="noreferrer">
Documentation
</a>
</div>
<div className="action">
<a target="_blank" rel="noreferrer" onClick={ipcHandle}>
Send IPC
</a>
</div>
</div>
<Versions></Versions>
</>
)
}
export default App
-67
View File
@@ -1,67 +0,0 @@
:root {
--ev-c-white: #ffffff;
--ev-c-white-soft: #f8f8f8;
--ev-c-white-mute: #f2f2f2;
--ev-c-black: #1b1b1f;
--ev-c-black-soft: #222222;
--ev-c-black-mute: #282828;
--ev-c-gray-1: #515c67;
--ev-c-gray-2: #414853;
--ev-c-gray-3: #32363f;
--ev-c-text-1: rgba(255, 255, 245, 0.86);
--ev-c-text-2: rgba(235, 235, 245, 0.6);
--ev-c-text-3: rgba(235, 235, 245, 0.38);
--ev-button-alt-border: transparent;
--ev-button-alt-text: var(--ev-c-text-1);
--ev-button-alt-bg: var(--ev-c-gray-3);
--ev-button-alt-hover-border: transparent;
--ev-button-alt-hover-text: var(--ev-c-text-1);
--ev-button-alt-hover-bg: var(--ev-c-gray-2);
}
:root {
--color-background: var(--ev-c-black);
--color-background-soft: var(--ev-c-black-soft);
--color-background-mute: var(--ev-c-black-mute);
--color-text: var(--ev-c-text-1);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
ul {
list-style: none;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -1,10 +0,0 @@
<svg viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="64" cy="64" r="64" fill="#2F3242"/>
<ellipse cx="63.9835" cy="23.2036" rx="4.48794" ry="4.495" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path d="M51.3954 39.5028C52.3733 39.6812 53.3108 39.033 53.4892 38.055C53.6676 37.0771 53.0194 36.1396 52.0414 35.9612L51.3954 39.5028ZM28.6153 43.5751L30.1748 44.4741L30.1748 44.4741L28.6153 43.5751ZM28.9393 60.9358C29.4332 61.7985 30.5329 62.0976 31.3957 61.6037C32.2585 61.1098 32.5575 60.0101 32.0636 59.1473L28.9393 60.9358ZM37.6935 66.7457C37.025 66.01 35.8866 65.9554 35.1508 66.6239C34.415 67.2924 34.3605 68.4308 35.029 69.1666L37.6935 66.7457ZM53.7489 81.7014L52.8478 83.2597L53.7489 81.7014ZM96.9206 89.515C97.7416 88.9544 97.9526 87.8344 97.3919 87.0135C96.8313 86.1925 95.7113 85.9815 94.8904 86.5422L96.9206 89.515ZM52.0414 35.9612C46.4712 34.9451 41.2848 34.8966 36.9738 35.9376C32.6548 36.9806 29.0841 39.1576 27.0559 42.6762L30.1748 44.4741C31.5693 42.0549 34.1448 40.3243 37.8188 39.4371C41.5009 38.5479 46.1547 38.5468 51.3954 39.5028L52.0414 35.9612ZM27.0559 42.6762C24.043 47.9029 25.2781 54.5399 28.9393 60.9358L32.0636 59.1473C28.6579 53.1977 28.1088 48.0581 30.1748 44.4741L27.0559 42.6762ZM35.029 69.1666C39.6385 74.24 45.7158 79.1355 52.8478 83.2597L54.6499 80.1432C47.8081 76.1868 42.0298 71.5185 37.6935 66.7457L35.029 69.1666ZM52.8478 83.2597C61.344 88.1726 70.0465 91.2445 77.7351 92.3608C85.359 93.4677 92.2744 92.6881 96.9206 89.515L94.8904 86.5422C91.3255 88.9767 85.4902 89.849 78.2524 88.7982C71.0793 87.7567 62.809 84.8612 54.6499 80.1432L52.8478 83.2597ZM105.359 84.9077C105.359 81.4337 102.546 78.6127 99.071 78.6127V82.2127C100.553 82.2127 101.759 83.4166 101.759 84.9077H105.359ZM99.071 78.6127C95.5956 78.6127 92.7831 81.4337 92.7831 84.9077H96.3831C96.3831 83.4166 97.5892 82.2127 99.071 82.2127V78.6127ZM92.7831 84.9077C92.7831 88.3817 95.5956 91.2027 99.071 91.2027V87.6027C97.5892 87.6027 96.3831 86.3988 96.3831 84.9077H92.7831ZM99.071 91.2027C102.546 91.2027 105.359 88.3817 105.359 84.9077H101.759C101.759 86.3988 100.553 87.6027 99.071 87.6027V91.2027Z" fill="#A2ECFB"/>
<path d="M91.4873 65.382C90.8456 66.1412 90.9409 67.2769 91.7002 67.9186C92.4594 68.5603 93.5951 68.465 94.2368 67.7058L91.4873 65.382ZM99.3169 43.6354L97.7574 44.5344L99.3169 43.6354ZM84.507 35.2412C83.513 35.2282 82.6967 36.0236 82.6838 37.0176C82.6708 38.0116 83.4661 38.8279 84.4602 38.8409L84.507 35.2412ZM74.9407 39.8801C75.9127 39.6716 76.5315 38.7145 76.323 37.7425C76.1144 36.7706 75.1573 36.1517 74.1854 36.3603L74.9407 39.8801ZM53.7836 46.3728L54.6847 47.931L53.7836 46.3728ZM25.5491 80.9047C25.6932 81.8883 26.6074 82.5688 27.5911 82.4247C28.5747 82.2806 29.2552 81.3664 29.1111 80.3828L25.5491 80.9047ZM94.2368 67.7058C97.8838 63.3907 100.505 58.927 101.752 54.678C103.001 50.4213 102.9 46.2472 100.876 42.7365L97.7574 44.5344C99.1494 46.9491 99.3603 50.0419 98.2974 53.6644C97.2323 57.2945 94.9184 61.3223 91.4873 65.382L94.2368 67.7058ZM100.876 42.7365C97.9119 37.5938 91.7082 35.335 84.507 35.2412L84.4602 38.8409C91.1328 38.9278 95.7262 41.0106 97.7574 44.5344L100.876 42.7365ZM74.1854 36.3603C67.4362 37.8086 60.0878 40.648 52.8826 44.8146L54.6847 47.931C61.5972 43.9338 68.5948 41.2419 74.9407 39.8801L74.1854 36.3603ZM52.8826 44.8146C44.1366 49.872 36.9669 56.0954 32.1491 62.3927C27.3774 68.63 24.7148 75.2115 25.5491 80.9047L29.1111 80.3828C28.4839 76.1026 30.4747 70.5062 35.0084 64.5802C39.496 58.7143 46.2839 52.7889 54.6847 47.931L52.8826 44.8146Z" fill="#A2ECFB"/>
<path d="M49.0825 87.2295C48.7478 86.2934 47.7176 85.8059 46.7816 86.1406C45.8455 86.4753 45.358 87.5055 45.6927 88.4416L49.0825 87.2295ZM78.5635 96.4256C79.075 95.5732 78.7988 94.4675 77.9464 93.9559C77.0941 93.4443 75.9884 93.7205 75.4768 94.5729L78.5635 96.4256ZM79.5703 85.1795C79.2738 86.1284 79.8027 87.1379 80.7516 87.4344C81.7004 87.7308 82.71 87.2019 83.0064 86.2531L79.5703 85.1795ZM84.3832 64.0673H82.5832H84.3832ZM69.156 22.5301C68.2477 22.1261 67.1838 22.535 66.7799 23.4433C66.3759 24.3517 66.7848 25.4155 67.6931 25.8194L69.156 22.5301ZM45.6927 88.4416C47.5994 93.7741 50.1496 98.2905 53.2032 101.505C56.2623 104.724 59.9279 106.731 63.9835 106.731V103.131C61.1984 103.131 58.4165 101.765 55.8131 99.0249C53.2042 96.279 50.8768 92.2477 49.0825 87.2295L45.6927 88.4416ZM63.9835 106.731C69.8694 106.731 74.8921 102.542 78.5635 96.4256L75.4768 94.5729C72.0781 100.235 68.0122 103.131 63.9835 103.131V106.731ZM83.0064 86.2531C85.0269 79.7864 86.1832 72.1831 86.1832 64.0673H82.5832C82.5832 71.8536 81.4723 79.0919 79.5703 85.1795L83.0064 86.2531ZM86.1832 64.0673C86.1832 54.1144 84.4439 44.922 81.4961 37.6502C78.5748 30.4436 74.3436 24.8371 69.156 22.5301L67.6931 25.8194C71.6364 27.5731 75.3846 32.1564 78.1598 39.0026C80.9086 45.7836 82.5832 54.507 82.5832 64.0673H86.1832Z" fill="#A2ECFB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M103.559 84.9077C103.559 82.4252 101.55 80.4127 99.071 80.4127C96.5924 80.4127 94.5831 82.4252 94.5831 84.9077C94.5831 87.3902 96.5924 89.4027 99.071 89.4027C101.55 89.4027 103.559 87.3902 103.559 84.9077V84.9077Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8143 89.4027C31.2929 89.4027 33.3023 87.3902 33.3023 84.9077C33.3023 82.4252 31.2929 80.4127 28.8143 80.4127C26.3357 80.4127 24.3264 82.4252 24.3264 84.9077C24.3264 87.3902 26.3357 89.4027 28.8143 89.4027V89.4027V89.4027Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.8501 68.0857C62.6341 68.5652 60.451 67.1547 59.9713 64.9353C59.4934 62.7159 60.9007 60.5293 63.1167 60.0489C65.3326 59.5693 67.5157 60.9798 67.9954 63.1992C68.4742 65.4186 67.066 67.6052 64.8501 68.0857Z" fill="#A2ECFB"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

-171
View File
@@ -1,171 +0,0 @@
@import './base.css';
body {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background-image: url('./wavy-lines.svg');
background-size: cover;
user-select: none;
}
code {
font-weight: 600;
padding: 3px 5px;
border-radius: 2px;
background-color: var(--color-background-mute);
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 85%;
}
#root {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 80px;
}
.logo {
margin-bottom: 20px;
-webkit-user-drag: none;
height: 128px;
width: 128px;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 1.2em #6988e6aa);
}
.creator {
font-size: 14px;
line-height: 16px;
color: var(--ev-c-text-2);
font-weight: 600;
margin-bottom: 10px;
}
.text {
font-size: 28px;
color: var(--ev-c-text-1);
font-weight: 700;
line-height: 32px;
text-align: center;
margin: 0 10px;
padding: 16px 0;
}
.tip {
font-size: 16px;
line-height: 24px;
color: var(--ev-c-text-2);
font-weight: 600;
}
.react {
background: -webkit-linear-gradient(315deg, #087ea4 55%, #7c93ee);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
}
.ts {
background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
}
.actions {
display: flex;
padding-top: 32px;
margin: -6px;
flex-wrap: wrap;
justify-content: flex-start;
}
.action {
flex-shrink: 0;
padding: 6px;
}
.action a {
cursor: pointer;
text-decoration: none;
display: inline-block;
border: 1px solid transparent;
text-align: center;
font-weight: 600;
white-space: nowrap;
border-radius: 20px;
padding: 0 20px;
line-height: 38px;
font-size: 14px;
border-color: var(--ev-button-alt-border);
color: var(--ev-button-alt-text);
background-color: var(--ev-button-alt-bg);
}
.action a:hover {
border-color: var(--ev-button-alt-hover-border);
color: var(--ev-button-alt-hover-text);
background-color: var(--ev-button-alt-hover-bg);
}
.versions {
position: absolute;
bottom: 30px;
margin: 0 auto;
padding: 15px 0;
font-family: 'Menlo', 'Lucida Console', monospace;
display: inline-flex;
overflow: hidden;
align-items: center;
border-radius: 22px;
background-color: #202127;
backdrop-filter: blur(24px);
}
.versions li {
display: block;
float: left;
border-right: 1px solid var(--ev-c-gray-1);
padding: 0 20px;
font-size: 14px;
line-height: 14px;
opacity: 0.8;
&:last-child {
border: none;
}
}
@media (max-width: 720px) {
.text {
font-size: 20px;
}
}
@media (max-width: 620px) {
.versions {
display: none;
}
}
@media (max-width: 350px) {
.tip,
.actions {
display: none;
}
}
@@ -1,25 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1422 800" opacity="0.3">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="oooscillate-grad">
<stop stop-color="hsl(206, 75%, 49%)" stop-opacity="1" offset="0%"></stop>
<stop stop-color="hsl(331, 90%, 56%)" stop-opacity="1" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke-width="1" stroke="url(#oooscillate-grad)" fill="none" stroke-linecap="round">
<path d="M 0 448 Q 355.5 -100 711 400 Q 1066.5 900 1422 448" opacity="0.05"></path>
<path d="M 0 420 Q 355.5 -100 711 400 Q 1066.5 900 1422 420" opacity="0.11"></path>
<path d="M 0 392 Q 355.5 -100 711 400 Q 1066.5 900 1422 392" opacity="0.18"></path>
<path d="M 0 364 Q 355.5 -100 711 400 Q 1066.5 900 1422 364" opacity="0.24"></path>
<path d="M 0 336 Q 355.5 -100 711 400 Q 1066.5 900 1422 336" opacity="0.30"></path>
<path d="M 0 308 Q 355.5 -100 711 400 Q 1066.5 900 1422 308" opacity="0.37"></path>
<path d="M 0 280 Q 355.5 -100 711 400 Q 1066.5 900 1422 280" opacity="0.43"></path>
<path d="M 0 252 Q 355.5 -100 711 400 Q 1066.5 900 1422 252" opacity="0.49"></path>
<path d="M 0 224 Q 355.5 -100 711 400 Q 1066.5 900 1422 224" opacity="0.56"></path>
<path d="M 0 196 Q 355.5 -100 711 400 Q 1066.5 900 1422 196" opacity="0.62"></path>
<path d="M 0 168 Q 355.5 -100 711 400 Q 1066.5 900 1422 168" opacity="0.68"></path>
<path d="M 0 140 Q 355.5 -100 711 400 Q 1066.5 900 1422 140" opacity="0.75"></path>
<path d="M 0 112 Q 355.5 -100 711 400 Q 1066.5 900 1422 112" opacity="0.81"></path>
<path d="M 0 84 Q 355.5 -100 711 400 Q 1066.5 900 1422 84" opacity="0.87"></path>
<path d="M 0 56 Q 355.5 -100 711 400 Q 1066.5 900 1422 56" opacity="0.94"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,15 +0,0 @@
import { useState } from 'react'
function Versions(): JSX.Element {
const [versions] = useState(window.electron.process.versions)
return (
<ul className="versions">
<li className="electron-version">Electron v{versions.electron}</li>
<li className="chrome-version">Chromium v{versions.chrome}</li>
<li className="node-version">Node v{versions.node}</li>
</ul>
)
}
export default Versions
-1
View File
@@ -1 +0,0 @@
/// <reference types="vite/client" />
-11
View File
@@ -1,11 +0,0 @@
import './assets/main.css'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
+15
View File
@@ -0,0 +1,15 @@
import React from "react";
import BaseLayout from "@/layouts/BaseLayout";
import { Outlet, createRootRoute } from "@tanstack/react-router";
export const RootRoute = createRootRoute({
component: Root,
});
function Root() {
return (
<BaseLayout>
<Outlet />
</BaseLayout>
);
}
+13
View File
@@ -0,0 +1,13 @@
import { createMemoryHistory, createRouter } from "@tanstack/react-router";
import { rootTree } from "./routes";
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
const history = createMemoryHistory({
initialEntries: ["/"],
});
export const router = createRouter({ routeTree: rootTree, history: history });
+37
View File
@@ -0,0 +1,37 @@
import { createRoute } from "@tanstack/react-router";
import { RootRoute } from "./__root";
import HomePage from "../pages/HomePage";
import SecondPage from "@/pages/SecondPage";
// TODO: Steps to add a new route:
// 1. Create a new page component in the '../pages/' directory (e.g., NewPage.tsx)
// 2. Import the new page component at the top of this file
// 3. Define a new route for the page using createRoute()
// 4. Add the new route to the routeTree in RootRoute.addChildren([...])
// 5. Add a new Link in the navigation section of RootRoute if needed
// Example of adding a new route:
// 1. Create '../pages/NewPage.tsx'
// 2. Import: import NewPage from '../pages/NewPage';
// 3. Define route:
// const NewRoute = createRoute({
// getParentRoute: () => RootRoute,
// path: '/new',
// component: NewPage,
// });
// 4. Add to routeTree: RootRoute.addChildren([HomeRoute, NewRoute, ...])
// 5. Add Link: <Link to="/new">New Page</Link>
export const HomeRoute = createRoute({
getParentRoute: () => RootRoute,
path: "/",
component: HomePage,
});
export const SecondPageRoute = createRoute({
getParentRoute: () => RootRoute,
path: "/second-page",
component: SecondPage,
});
export const rootTree = RootRoute.addChildren([HomeRoute, SecondPageRoute]);
+196
View File
@@ -0,0 +1,196 @@
@import 'tailwindcss';
@plugin 'tailwindcss-animate';
@custom-variant dark (&:is(.dark *));
@theme {
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--font-sans: Geist, sans-serif;
--font-mono: Geist Mono, monospace;
--font-tomorrow: Tomorrow, sans-serif;
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >= --theme(--breakpoint-sm)) {
max-width: none;
}
@media (width >= 1400px) {
max-width: 1400px;
}
}
@layer base {
@font-face {
font-family: "Geist";
src: url("../assets/fonts/geist/geist.ttf") format("truetype");
}
@font-face {
font-family: "Geist Mono";
font-display: swap;
src: url("../assets/fonts/geist-mono/geist-mono.ttf") format("truetype");
}
@font-face {
font-family: "Tomorrow";
font-weight: 400;
font-style: normal;
src: url("../assets/fonts/tomorrow/tomorrow-regular.ttf") format("truetype");
}
@font-face {
font-family: "Tomorrow";
font-weight: 400;
font-style: italic;
src: url("../assets/fonts/tomorrow/tomorrow-italic.ttf") format("truetype");
}
@font-face {
font-family: "Tomorrow";
font-weight: 700;
font-style: normal;
src: url("../assets/fonts/tomorrow/tomorrow-bold.ttf") format("truetype");
}
@font-face {
font-family: "Tomorrow";
font-weight: 700;
font-style: italic;
src: url("../assets/fonts/tomorrow/tomorrow-bold-italic.ttf") format("truetype");
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply m-0 border-border p-0;
}
body {
@apply overflow-hidden bg-background text-foreground;
}
.draglayer {
@apply bg-background;
-webkit-app-region: drag;
}
button {
@apply cursor-pointer;
}
}
+51
View File
@@ -0,0 +1,51 @@
import {
test,
expect,
_electron as electron,
ElectronApplication,
Page,
} from "@playwright/test";
import { findLatestBuild, parseElectronApp } from "electron-playwright-helpers";
/*
* Using Playwright with Electron:
* https://www.electronjs.org/pt/docs/latest/tutorial/automated-testing#using-playwright
*/
let electronApp: ElectronApplication;
test.beforeAll(async () => {
const latestBuild = findLatestBuild();
const appInfo = parseElectronApp(latestBuild);
process.env.CI = "e2e";
electronApp = await electron.launch({
args: [appInfo.main],
});
electronApp.on("window", async (page) => {
const filename = page.url()?.split("/").pop();
console.log(`Window opened: ${filename}`);
page.on("pageerror", (error) => {
console.error(error);
});
page.on("console", (msg) => {
console.log(msg.text());
});
});
});
test("renders the first page", async () => {
const page: Page = await electronApp.firstWindow();
const title = await page.waitForSelector("h1");
const text = await title.textContent();
expect(text).toBe("electron-shadcn");
});
test("renders page name", async () => {
const page: Page = await electronApp.firstWindow();
await page.waitForSelector("h1");
const pageName = await page.getByTestId("pageTitle");
const text = await pageName.textContent();
expect(text).toBe("Home Page");
});
@@ -0,0 +1,27 @@
import { render } from "@testing-library/react";
import { test, expect } from "vitest";
import ToggleTheme from "@/components/ToggleTheme";
import React from "react";
test("renders ToggleTheme", () => {
const { getByRole } = render(<ToggleTheme />);
const isButton = getByRole("button");
expect(isButton).toBeInTheDocument();
});
test("has icon", () => {
const { getByRole } = render(<ToggleTheme />);
const button = getByRole("button");
const icon = button.querySelector("svg");
expect(icon).toBeInTheDocument();
});
test("is moon icon", () => {
const svgIconClassName: string = "lucide-moon";
const { getByRole } = render(<ToggleTheme />);
const svg = getByRole("button").querySelector("svg");
expect(svg?.classList).toContain(svgIconClassName);
});
+1
View File
@@ -0,0 +1 @@
import "@testing-library/jest-dom";
+14
View File
@@ -0,0 +1,14 @@
import { expect, test } from "vitest";
function sum(a: number, b: number): number {
return a + b;
}
test("sum", () => {
const param1: number = 2;
const param2: number = 2;
const result: number = sum(param1, param2);
expect(result).toBe(4);
});
+24
View File
@@ -0,0 +1,24 @@
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite
// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on
// whether you're running in development or production).
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
declare const MAIN_WINDOW_VITE_NAME: string;
// Preload types
interface ThemeModeContext {
toggle: () => Promise<boolean>;
dark: () => Promise<void>;
light: () => Promise<void>;
system: () => Promise<boolean>;
current: () => Promise<"dark" | "light" | "system">;
}
interface ElectronWindow {
minimize: () => Promise<void>;
maximize: () => Promise<void>;
close: () => Promise<void>;
}
declare interface Window {
themeMode: ThemeModeContext;
electronWindow: ElectronWindow;
}
+1
View File
@@ -0,0 +1 @@
export type ThemeMode = "dark" | "light" | "system";
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+23 -2
View File
@@ -1,4 +1,25 @@
{
"files": [],
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
"compilerOptions": {
"jsx": "react",
"target": "ESNext",
"module": "CommonJS",
"lib": ["dom", "ESNext"],
"experimentalDecorators": true,
"composite": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"allowJs": false,
"esModuleInterop": true,
"noImplicitAny": true,
"sourceMap": true,
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"outDir": "dist",
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": ["src/**/*"]
}
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"]
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
"include": [
"src/renderer/src/env.d.ts",
"src/renderer/src/**/*",
"src/renderer/src/**/*.tsx",
"src/preload/*.d.ts"
],
"compilerOptions": {
"composite": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@renderer/*": [
"src/renderer/src/*"
]
}
}
}
+110
View File
@@ -0,0 +1,110 @@
import { builtinModules } from "node:module";
import type { AddressInfo } from "node:net";
import type { ConfigEnv, Plugin, UserConfig } from "vite";
import pkg from "./package.json";
export const builtins = [
"electron",
...builtinModules.map((m) => [m, `node:${m}`]).flat(),
];
export const external = [
...builtins,
...Object.keys(
"dependencies" in pkg ? (pkg.dependencies as Record<string, unknown>) : {},
),
];
export function getBuildConfig(env: ConfigEnv<"build">): UserConfig {
const { root, mode, command } = env;
return {
root,
mode,
build: {
// Prevent multiple builds from interfering with each other.
emptyOutDir: false,
// 🚧 Multiple builds may conflict.
outDir: ".vite/build",
watch: command === "serve" ? {} : null,
minify: command === "build",
},
clearScreen: false,
};
}
export function getDefineKeys(names: string[]) {
const define: { [name: string]: VitePluginRuntimeKeys } = {};
return names.reduce((acc, name) => {
const NAME = name.toUpperCase();
const keys: VitePluginRuntimeKeys = {
VITE_DEV_SERVER_URL: `${NAME}_VITE_DEV_SERVER_URL`,
VITE_NAME: `${NAME}_VITE_NAME`,
};
return { ...acc, [name]: keys };
}, define);
}
export function getBuildDefine(env: ConfigEnv<"build">) {
const { command, forgeConfig } = env;
const names = forgeConfig.renderer
.filter(({ name }) => name != null)
.map(({ name }) => name!);
const defineKeys = getDefineKeys(names);
const define = Object.entries(defineKeys).reduce(
(acc, [name, keys]) => {
const { VITE_DEV_SERVER_URL, VITE_NAME } = keys;
const def = {
[VITE_DEV_SERVER_URL]:
command === "serve"
? JSON.stringify(process.env[VITE_DEV_SERVER_URL])
: undefined,
[VITE_NAME]: JSON.stringify(name),
};
return { ...acc, ...def };
},
{} as Record<string, any>,
);
return define;
}
export function pluginExposeRenderer(name: string): Plugin {
const { VITE_DEV_SERVER_URL } = getDefineKeys([name])[name];
return {
name: "@electron-forge/plugin-vite:expose-renderer",
configureServer(server) {
process.viteDevServers ??= {};
// Expose server for preload scripts hot reload.
process.viteDevServers[name] = server;
server.httpServer?.once("listening", () => {
const addressInfo = server.httpServer!.address() as AddressInfo;
// Expose env constant for main process use.
process.env[VITE_DEV_SERVER_URL] =
`http://localhost:${addressInfo?.port}`;
});
},
};
}
export function pluginHotRestart(command: "reload" | "restart"): Plugin {
return {
name: "@electron-forge/plugin-vite:hot-restart",
closeBundle() {
if (command === "reload") {
for (const server of Object.values(process.viteDevServers)) {
// Preload scripts hot reload.
server.ws.send({ type: "full-reload" });
}
} else {
// Main process hot restart.
// https://github.com/electron/forge/blob/v7.2.0/packages/api/core/src/api/start.ts#L216-L223
process.stdin.emit("data", "rs");
}
},
};
}
+35
View File
@@ -0,0 +1,35 @@
import type { ConfigEnv, UserConfig } from "vite";
import { defineConfig, mergeConfig } from "vite";
import {
getBuildConfig,
getBuildDefine,
external,
pluginHotRestart,
} from "./vite.base.config";
// https://vitejs.dev/config
export default defineConfig((env) => {
const forgeEnv = env as ConfigEnv<"build">;
const { forgeConfigSelf } = forgeEnv;
const define = getBuildDefine(forgeEnv);
const config: UserConfig = {
build: {
lib: {
entry: forgeConfigSelf.entry!,
fileName: () => "[name].js",
formats: ["cjs"],
},
rollupOptions: {
external,
},
},
plugins: [pluginHotRestart("restart")],
define,
resolve: {
// Load the Node.js entry.
mainFields: ["module", "jsnext:main", "jsnext"],
},
};
return mergeConfig(getBuildConfig(forgeEnv), config);
});
+29
View File
@@ -0,0 +1,29 @@
import type { ConfigEnv, UserConfig } from "vite";
import { defineConfig, mergeConfig } from "vite";
import { getBuildConfig, external, pluginHotRestart } from "./vite.base.config";
// https://vitejs.dev/config
export default defineConfig((env) => {
const forgeEnv = env as ConfigEnv<"build">;
const { forgeConfigSelf } = forgeEnv;
const config: UserConfig = {
build: {
rollupOptions: {
external,
// Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.
input: forgeConfigSelf.entry!,
output: {
format: "cjs",
// It should not be split chunks.
inlineDynamicImports: true,
entryFileNames: "[name].js",
chunkFileNames: "[name].js",
assetFileNames: "[name].[ext]",
},
},
},
plugins: [pluginHotRestart("reload")],
};
return mergeConfig(getBuildConfig(forgeEnv), config);
});
+36
View File
@@ -0,0 +1,36 @@
import path from "path";
import react from "@vitejs/plugin-react";
import type { ConfigEnv, UserConfig } from "vite";
import { defineConfig } from "vite";
import { pluginExposeRenderer } from "./vite.base.config";
// https://vitejs.dev/config
export default defineConfig((env) => {
const forgeEnv = env as ConfigEnv<"renderer">;
const { root, mode, forgeConfigSelf } = forgeEnv;
const name = forgeConfigSelf.name ?? "";
return {
root,
mode,
base: "./",
build: {
outDir: `.vite/renderer/${name}`,
},
plugins: [
pluginExposeRenderer(name),
react({
babel: {
plugins: [["babel-plugin-react-compiler"]],
},
}),
],
resolve: {
preserveSymlinks: true,
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
clearScreen: false,
} as UserConfig;
});
+26
View File
@@ -0,0 +1,26 @@
import path from "path";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
test: {
dir: "./src/tests/unit",
globals: true,
environment: "jsdom",
setupFiles: "./src/tests/unit/setup.ts",
css: true,
reporters: ["verbose"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
include: ["src/**/*"],
exclude: [],
},
},
});