├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ └── release-tag.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.yaml
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── package.json
├── packages
├── contextmenu
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.ts
│ ├── src
│ │ ├── common.ts
│ │ ├── constants.ts
│ │ ├── main.ts
│ │ ├── renderer.ts
│ │ └── types.ts
│ └── tsconfig.json
├── core
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── globals.d.ts
│ ├── package.json
│ ├── rollup.config.ts
│ ├── src
│ │ ├── global.ts
│ │ ├── main.ts
│ │ └── preload.ts
│ └── tsconfig.json
├── notification
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.ts
│ ├── screenshots
│ │ ├── customization.png
│ │ └── default.png
│ ├── src
│ │ ├── common.ts
│ │ ├── constants.ts
│ │ ├── icon.svg
│ │ ├── index.html
│ │ ├── main.ts
│ │ ├── preload.ts
│ │ ├── renderer.ts
│ │ └── types.ts
│ └── tsconfig.json
├── playground
│ ├── .env.preload
│ ├── README.md
│ ├── electron.vite.config.ts
│ ├── package.json
│ ├── src
│ │ ├── main
│ │ │ ├── env.d.ts
│ │ │ └── index.ts
│ │ ├── preload
│ │ │ └── index.ts
│ │ └── renderer
│ │ │ ├── index.html
│ │ │ ├── public
│ │ │ ├── moon.svg
│ │ │ └── sun.svg
│ │ │ └── src
│ │ │ ├── env.d.ts
│ │ │ └── index.ts
│ └── tsconfig.json
├── titlebar
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── custom-elements.json
│ ├── package.json
│ ├── rollup.config.ts
│ ├── screenshots
│ │ ├── mac-light.png
│ │ └── win32-dark.png
│ ├── src
│ │ ├── common.ts
│ │ ├── constants.ts
│ │ ├── main.ts
│ │ └── renderer.ts
│ └── tsconfig.json
└── toast
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.ts
│ ├── screenshots
│ └── toast.png
│ ├── src
│ ├── common.ts
│ ├── constants.ts
│ ├── main.ts
│ ├── renderer.ts
│ └── types.ts
│ └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── tsconfig.base.json
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | out
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | commonjs: true,
5 | es6: true,
6 | node: true
7 | },
8 | parser: '@typescript-eslint/parser',
9 | parserOptions: {
10 | sourceType: 'module',
11 | ecmaVersion: 2022
12 | },
13 | plugins: ['@typescript-eslint'],
14 | extends: [
15 | 'eslint:recommended',
16 | 'plugin:@typescript-eslint/recommended',
17 | 'plugin:@typescript-eslint/eslint-recommended',
18 | 'plugin:prettier/recommended'
19 | ],
20 | rules: {
21 | 'prettier/prettier': 'warn',
22 | '@typescript-eslint/ban-ts-comment': [
23 | 'error',
24 | { 'ts-ignore': 'allow-with-description' }
25 | ],
26 | '@typescript-eslint/explicit-function-return-type': 'error',
27 | '@typescript-eslint/explicit-module-boundary-types': 'off',
28 | '@typescript-eslint/no-empty-function': [
29 | 'error',
30 | { allow: ['arrowFunctions'] }
31 | ],
32 | '@typescript-eslint/no-explicit-any': 'error',
33 | '@typescript-eslint/no-non-null-assertion': 'off',
34 | '@typescript-eslint/no-var-requires': 'off'
35 | },
36 | overrides: [
37 | {
38 | files: ['*.js', '*.mjs'],
39 | rules: {
40 | '@typescript-eslint/explicit-function-return-type': 'off'
41 | }
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/release-tag.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | tags:
4 | - 'core*'
5 | - 'titlebar*'
6 | - 'contextmenu*'
7 | - 'notification*'
8 | - 'toast*'
9 |
10 | name: Create Release
11 |
12 | jobs:
13 | build:
14 | name: Create Release
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@master
19 | - name: Get package name for tag
20 | id: tag
21 | run: |
22 | pkgName=${GITHUB_REF_NAME%@*}
23 | echo "::set-output name=pkgName::$pkgName"
24 | - name: Create Release for Tag
25 | id: release_tag
26 | uses: actions/create-release@v1
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | with:
30 | tag_name: ${{ github.ref }}
31 | release_name: ${{ github.ref }}
32 | body: |
33 | Please refer to [CHANGELOG.md](https://github.com/alex8088/electron-uikit/blob/${{ github.ref_name }}/packages/${{ steps.tag.outputs.pkgName }}/CHANGELOG.md) for details.
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | out
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | auto-install-peers=false
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | pnpm-lock.yaml
2 | packages/**/dist
3 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | singleQuote: true
2 | semi: false
3 | printWidth: 80
4 | trailingComma: none
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "[typescript]": {
4 | "editor.defaultFormatter": "esbenp.prettier-vscode"
5 | },
6 | "[javascript]": {
7 | "editor.defaultFormatter": "esbenp.prettier-vscode"
8 | },
9 | "[json]": {
10 | "editor.defaultFormatter": "esbenp.prettier-vscode"
11 | },
12 | "html.customData": ["./packages/titlebar/custom-elements.json"]
13 | }
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024-present, Alex.Wei
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
electron-uikit
2 |
3 | UI kit for Electron. e.g. titlebar, contextmenu, notification, etc.
4 |
5 |
6 |
7 |
8 |
9 |
10 | ## Packages
11 |
12 | | Package | Description | Version |
13 | | ----------------------------------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------- |
14 | | [@electron-uikit/core](packages/core) | Electron UI kit core | [](packages/core/CHANGELOG.md) |
15 | | [@electron-uikit/contextmenu](packages/contextmenu) | Context menu | [](packages/contextmenu/CHANGELOG.md) |
16 | | [@electron-uikit/notification](packages/notification) | Notification | [](packages/notification/CHANGELOG.md) |
17 | | [@electron-uikit/titlebar](packages/titlebar) | Title bar web component | [](packages/titlebar/CHANGELOG.md) |
18 | | [@electron-uikit/toast](packages/toast) | Toast | [](packages/toast/CHANGELOG.md) |
19 |
20 | ## License
21 |
22 | [MIT](./LICENSE)
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "electron-uikit",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "UI kit for Electron. e.g. titlebar, contextmenu, notification, etc.",
6 | "license": "MIT",
7 | "author": "Alex Wei",
8 | "scripts": {
9 | "preinstall": "npx only-allow pnpm",
10 | "format": "prettier --write .",
11 | "lint": "eslint --ext .ts packages/*/src/**",
12 | "typecheck": "tsc --noEmit",
13 | "play": "pnpm run -C packages/playground dev",
14 | "play:preload": "pnpm run -C packages/playground dev:preload",
15 | "build:core": "pnpm run -C packages/core build",
16 | "build:contextmenu": "pnpm run -C packages/contextmenu build",
17 | "build:notification": "pnpm run -C packages/notification build",
18 | "build:titlebar": "pnpm run -C packages/titlebar build",
19 | "build:toast": "pnpm run -C packages/toast build"
20 | },
21 | "devDependencies": {
22 | "@rollup/plugin-commonjs": "^26.0.1",
23 | "@rollup/plugin-node-resolve": "^15.2.3",
24 | "@rollup/plugin-typescript": "^11.1.6",
25 | "@types/node": "^18.19.34",
26 | "@typescript-eslint/eslint-plugin": "^7.13.0",
27 | "@typescript-eslint/parser": "^7.13.0",
28 | "electron": "^35.0.2",
29 | "eslint": "^8.57.0",
30 | "eslint-config-prettier": "^9.1.0",
31 | "eslint-plugin-prettier": "^5.1.3",
32 | "prettier": "^3.3.1",
33 | "rollup": "^4.18.0",
34 | "rollup-plugin-dts": "^6.1.1",
35 | "rollup-plugin-rm": "^1.0.2",
36 | "typescript": "^5.4.5"
37 | },
38 | "pnpm": {
39 | "peerDependencyRules": {
40 | "ignoreMissing": [
41 | "electron"
42 | ]
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/contextmenu/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### v1.0.0 (_2024-06-23_)
2 |
3 | -`@electron-uikit/contextmenu` init
4 |
--------------------------------------------------------------------------------
/packages/contextmenu/README.md:
--------------------------------------------------------------------------------
1 | # @electron-uikit/contextmenu
2 |
3 | 
4 |
5 | Context menu for Electron app. Inspired by VS Code.
6 |
7 | You can use the `Menu` class to create context menu in Electron, but it can only be created in the main process. But usually the context menu is triggered in the renderer process, which leads to code logic fragmentation, difficulty in reading, and additional IPC is required to respond to context menu operation processing results. UIKit context menu aims to solve these problems and provide a better DX.
8 |
9 | You can easily use the context menu in the renderer process and use the same way as Electron Menu.
10 |
11 | ## Usage
12 |
13 | ### Install
14 |
15 | ```sh
16 | npm i @electron-uikit/core @electron-uikit/contextmenu
17 | ```
18 |
19 | ### Get Started
20 |
21 | 1. Exposes the UI Kit APIs for components. See [@electron-uikit/core](https://github.com/alex8088/electron-uikit/tree/main/packages/core) guide for more details.
22 |
23 | You can expose it in the specified preload script:
24 |
25 | ```js
26 | import { exposeUIKit } from '@electron-uikit/core/preload'
27 |
28 | exposeUIKit()
29 | ```
30 |
31 | Or, you can expose it globally in the main process for all renderer processes:
32 |
33 | ```js
34 | import { useUIKit } from '@electron-uikit/core/main'
35 |
36 | useUIKit()
37 | ```
38 |
39 | > [!NOTE]
40 | > If you are using [@electron-toolkit/preload](https://github.com/alex8088/electron-toolkit/tree/master/packages/preload) to expose Electron APIs, there is no need to use this module, because `core` is also an export of it.
41 |
42 | 2. Register a listener in the main process, so that you can use it in the renderer process.
43 |
44 | ```js
45 | import { app } from 'electron'
46 | import { registerContextMenuListener } from '@electron-uikit/contextmenu'
47 |
48 | app.whenReady().then(() => {
49 | // Register context menu IPC listeners
50 | registerContextMenuListener()
51 |
52 | // ...
53 | })
54 | ```
55 |
56 | 3. Use the context menu in the renderer process.
57 |
58 | ```js
59 | import { Menu, MenuItem } from '@electron-uikit/contextmenu/renderer'
60 |
61 | document.body.addEventListener('contextmenu', (e) => {
62 | e.preventDefault()
63 |
64 | const menu = new Menu()
65 | menu.append(
66 | new MenuItem({
67 | type: 'checkbox',
68 | label: 'Menu Item One',
69 | checked: true,
70 | click: (): void => {
71 | console.log('menu item one')
72 | }
73 | })
74 | )
75 | menu.append(new MenuItem({ type: 'separator' }))
76 | menu.append(
77 | new MenuItem({
78 | label: 'Menu Item Two',
79 | click: (): void => {
80 | console.log('menu item two')
81 | }
82 | })
83 | )
84 | menu.popup()
85 | })
86 | ```
87 |
--------------------------------------------------------------------------------
/packages/contextmenu/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@electron-uikit/contextmenu",
3 | "version": "1.0.0",
4 | "description": "Context menu for Electron app.",
5 | "main": "dist/main.cjs",
6 | "module": "dist/main.mjs",
7 | "types": "dist/main.d.ts",
8 | "exports": {
9 | ".": {
10 | "types": "./dist/main.d.ts",
11 | "import": "./dist/main.mjs",
12 | "require": "./dist/main.cjs"
13 | },
14 | "./main": {
15 | "types": "./dist/main.d.ts",
16 | "import": "./dist/main.mjs",
17 | "require": "./dist/main.cjs"
18 | },
19 | "./renderer": {
20 | "types": "./dist/renderer.d.ts",
21 | "import": "./dist/renderer.mjs"
22 | }
23 | },
24 | "typesVersions": {
25 | "*": {
26 | "main": [
27 | "./dist/main.d.ts"
28 | ],
29 | "renderer": [
30 | "./dist/renderer.d.ts"
31 | ]
32 | }
33 | },
34 | "files": [
35 | "dist"
36 | ],
37 | "author": "Alex Wei",
38 | "license": "MIT",
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/alex8088/electron-uikit.git",
42 | "directory": "packages/contextmenu"
43 | },
44 | "bugs": {
45 | "url": "https://github.com/alex8088/electron-uikit/issues"
46 | },
47 | "homepage": "https://github.com/alex8088/electron-uikit/tree/master/packages/contextmenu#readme",
48 | "keywords": [
49 | "electron",
50 | "contextmenu"
51 | ],
52 | "scripts": {
53 | "build": "rollup -c rollup.config.ts --configPlugin typescript"
54 | },
55 | "peerDependencies": {
56 | "@electron-uikit/core": "*",
57 | "electron": ">=15.0.0"
58 | },
59 | "peerDependenciesMeta": {
60 | "@electron-uikit/core": {
61 | "optional": true
62 | }
63 | },
64 | "devDependencies": {
65 | "@electron-uikit/core": "workspace:^"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/contextmenu/rollup.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */
2 | import { defineConfig } from 'rollup'
3 | import resolve from '@rollup/plugin-node-resolve'
4 | import commonjs from '@rollup/plugin-commonjs'
5 | import ts from '@rollup/plugin-typescript'
6 | import dts from 'rollup-plugin-dts'
7 | import rm from 'rollup-plugin-rm'
8 |
9 | export default defineConfig([
10 | {
11 | input: ['src/main.ts'],
12 | output: [
13 | {
14 | entryFileNames: '[name].cjs',
15 | chunkFileNames: 'chunks/lib-[hash].cjs',
16 | format: 'cjs',
17 | dir: 'dist'
18 | },
19 | {
20 | entryFileNames: '[name].mjs',
21 | chunkFileNames: 'chunks/lib-[hash].mjs',
22 | format: 'es',
23 | dir: 'dist'
24 | }
25 | ],
26 | external: ['electron'],
27 | plugins: [
28 | resolve(),
29 | commonjs(),
30 | ts({
31 | compilerOptions: {
32 | rootDir: 'src',
33 | declaration: true,
34 | outDir: 'dist/types'
35 | }
36 | }),
37 | rm('dist', 'buildStart')
38 | ]
39 | },
40 | {
41 | input: ['src/renderer.ts'],
42 | output: [
43 | { file: './dist/renderer.mjs', format: 'es' },
44 | { name: 'renderer', file: './dist/renderer.js', format: 'iife' }
45 | ],
46 | plugins: [
47 | ts({
48 | compilerOptions: {
49 | rootDir: 'src',
50 | declaration: true,
51 | outDir: 'dist/types'
52 | }
53 | })
54 | ]
55 | },
56 | {
57 | input: ['dist/types/main.d.ts'],
58 | output: [{ file: './dist/main.d.ts', format: 'es' }],
59 | plugins: [dts()],
60 | external: ['electron']
61 | },
62 | {
63 | input: ['dist/types/renderer.d.ts'],
64 | output: [{ file: './dist/renderer.d.ts', format: 'es' }],
65 | plugins: [dts(), rm('dist/types', 'buildEnd')]
66 | }
67 | ])
68 |
--------------------------------------------------------------------------------
/packages/contextmenu/src/common.ts:
--------------------------------------------------------------------------------
1 | import type { UIKitAPI } from '@electron-uikit/core'
2 |
3 | export const core = ((globalThis || window).uikit ||
4 | (globalThis || window).electron) as UIKitAPI
5 |
--------------------------------------------------------------------------------
/packages/contextmenu/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const CONTEXT_MENU_CHANNEL = 'uikit:contextmenu'
2 | export const CONTEXT_MENU_CLOSE_CHANNEL = 'uikit:onCloseContextmenu'
3 |
--------------------------------------------------------------------------------
/packages/contextmenu/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ipcMain, IpcMainEvent, Menu, MenuItem, BrowserWindow } from 'electron'
2 | import { CONTEXT_MENU_CHANNEL, CONTEXT_MENU_CLOSE_CHANNEL } from './constants'
3 |
4 | import type { SerializableContextMenuItem, PopupOptions } from './types'
5 |
6 | /**
7 | * Register the context menu IPC listener for use by the renderer.
8 | */
9 | export function registerContextMenuListener(): void {
10 | if (ipcMain.eventNames().some((ename) => ename === CONTEXT_MENU_CHANNEL)) {
11 | return
12 | }
13 |
14 | ipcMain.on(
15 | CONTEXT_MENU_CHANNEL,
16 | (
17 | e,
18 | contextMenuId: number,
19 | items: SerializableContextMenuItem[],
20 | onClickChannel: string,
21 | options?: PopupOptions
22 | ) => {
23 | const menu = createMenu(e, onClickChannel, items)
24 |
25 | menu.popup({
26 | window: BrowserWindow.fromWebContents(e.sender) || undefined,
27 | x: options ? options.x : undefined,
28 | y: options ? options.y : undefined,
29 | positioningItem: options ? options.positioningItem : undefined,
30 | callback: () => {
31 | if (menu) {
32 | e.sender.send(CONTEXT_MENU_CLOSE_CHANNEL, contextMenuId)
33 | }
34 | }
35 | })
36 | }
37 | )
38 | }
39 |
40 | function createMenu(
41 | event: IpcMainEvent,
42 | onClickChannel: string,
43 | items: SerializableContextMenuItem[]
44 | ): Menu {
45 | const menu = new Menu()
46 |
47 | items.forEach((item) => {
48 | let menuItem: MenuItem
49 |
50 | // Separator
51 | if (item.type === 'separator') {
52 | menuItem = new MenuItem({
53 | type: item.type
54 | })
55 | }
56 |
57 | // Sub Menu
58 | else if (Array.isArray(item.submenu)) {
59 | menuItem = new MenuItem({
60 | submenu: createMenu(event, onClickChannel, item.submenu),
61 | label: item.label
62 | })
63 | }
64 |
65 | // Normal Menu Item
66 | else {
67 | menuItem = new MenuItem({
68 | label: item.label,
69 | type: item.type,
70 | accelerator: item.accelerator,
71 | checked: item.checked,
72 | enabled: item.enabled,
73 | visible: item.visible,
74 | click: (_menuItem, _win, contextmenuEvent): void =>
75 | event.sender.send(onClickChannel, item.id, contextmenuEvent)
76 | })
77 | }
78 |
79 | menu.append(menuItem)
80 | })
81 |
82 | return menu
83 | }
84 |
--------------------------------------------------------------------------------
/packages/contextmenu/src/renderer.ts:
--------------------------------------------------------------------------------
1 | import { core } from './common'
2 | import { CONTEXT_MENU_CHANNEL, CONTEXT_MENU_CLOSE_CHANNEL } from './constants'
3 | import type {
4 | ContextMenuItem,
5 | ContextMenuEvent,
6 | SerializableContextMenuItem,
7 | PopupOptions
8 | } from './types'
9 |
10 | export type { ContextMenuItem, ContextMenuEvent, PopupOptions } from './types'
11 |
12 | let contextMenuIdPool = 0
13 |
14 | /**
15 | * Pops up a context menu in the `BrowserWindow`.
16 | * @param items Menu items
17 | * @param options Popup options
18 | * @param onHide Called when menu is closed.
19 | */
20 | export function popup(
21 | items: ContextMenuItem[],
22 | options?: PopupOptions,
23 | onHide?: () => void
24 | ): void {
25 | const processedItems: ContextMenuItem[] = []
26 |
27 | const contextMenuId = contextMenuIdPool++
28 | const onClickChannel = `uikit:onContextMenu${contextMenuId}`
29 | const onClickChannelHandler = (
30 | _event: unknown,
31 | itemId: number,
32 | context: ContextMenuEvent
33 | ): void => {
34 | const item = processedItems[itemId]
35 | item.click?.(context)
36 | }
37 |
38 | const removeListener = core.ipcRenderer.once(
39 | onClickChannel,
40 | onClickChannelHandler
41 | )
42 | core.ipcRenderer.once(
43 | CONTEXT_MENU_CLOSE_CHANNEL,
44 | (_event: unknown, closedContextMenuId: number) => {
45 | if (closedContextMenuId !== contextMenuId) {
46 | return
47 | }
48 |
49 | removeListener()
50 |
51 | onHide?.()
52 | }
53 | )
54 |
55 | core.ipcRenderer.send(
56 | CONTEXT_MENU_CHANNEL,
57 | contextMenuId,
58 | items.map((item) => createItem(item, processedItems)),
59 | onClickChannel,
60 | options
61 | )
62 | }
63 |
64 | function createItem(
65 | item: ContextMenuItem,
66 | processedItems: ContextMenuItem[]
67 | ): SerializableContextMenuItem {
68 | const serializableItem: SerializableContextMenuItem = {
69 | id: processedItems.length,
70 | label: item.label,
71 | type: item.type,
72 | accelerator: item.accelerator,
73 | checked: item.checked,
74 | enabled: typeof item.enabled === 'boolean' ? item.enabled : true,
75 | visible: typeof item.visible === 'boolean' ? item.visible : true
76 | }
77 |
78 | processedItems.push(item)
79 |
80 | // Submenu
81 | if (Array.isArray(item.submenu)) {
82 | serializableItem.submenu = item.submenu.map((submenuItem) =>
83 | createItem(submenuItem, processedItems)
84 | )
85 | }
86 |
87 | return serializableItem
88 | }
89 |
90 | export type MenuItemOptions = ContextMenuItem
91 |
92 | export class MenuItem {
93 | constructor(public options: MenuItemOptions) {}
94 | }
95 |
96 | export class Menu {
97 | items: ContextMenuItem[] = []
98 |
99 | /**
100 | * Appends the `menuItem` to the menu.
101 | */
102 | append(item: MenuItem): void {
103 | this.items.push(item.options)
104 | }
105 |
106 | /**
107 | * Pops up this menu as a context menu in the `BrowserWindow`.
108 | */
109 | popup(options?: PopupOptions, onHide?: () => void): void {
110 | popup(this.items, options, onHide)
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/packages/contextmenu/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface CommonContextMenuItem {
2 | /**
3 | * A `string` indicating the item's visible label.
4 | */
5 | label?: string
6 | /**
7 | * A `string` indicating the type of the item. Can be `normal`, `separator`,
8 | * `submenu`, `checkbox` or `radio`.
9 | */
10 | type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'
11 | /**
12 | * An `Accelerator` (optional) indicating the item's accelerator, if set.
13 | */
14 | accelerator?: string
15 | /**
16 | * A `boolean` indicating whether the item is enabled.
17 | */
18 | enabled?: boolean
19 | /**
20 | * A `boolean` indicating whether the item is visible.
21 | */
22 | visible?: boolean
23 | /**
24 | * A `boolean` indicating whether the item is checked.
25 | *
26 | * A `checkbox` menu item will toggle the `checked` property on and off when
27 | * selected.
28 | *
29 | * A `radio` menu item will turn on its `checked` property when clicked, and will
30 | * turn off that property for all adjacent items in the same menu.
31 | *
32 | * You can add a `click` function for additional behavior.
33 | */
34 | checked?: boolean
35 | }
36 |
37 | export interface SerializableContextMenuItem extends CommonContextMenuItem {
38 | id: number
39 | submenu?: SerializableContextMenuItem[]
40 | }
41 |
42 | export interface ContextMenuItem extends CommonContextMenuItem {
43 | /**
44 | * A `Function` that is fired when the MenuItem receives a click event.
45 | */
46 | click?: (event: ContextMenuEvent) => void
47 | /**
48 | * A `Menu` (optional) containing the menu item's submenu, if present.
49 | */
50 | submenu?: ContextMenuItem[]
51 | }
52 |
53 | export interface ContextMenuEvent {
54 | shiftKey?: boolean
55 | ctrlKey?: boolean
56 | altKey?: boolean
57 | metaKey?: boolean
58 | }
59 |
60 | export interface PopupOptions {
61 | /**
62 | * Default is the current mouse cursor position. Must be declared if `y` is
63 | * declared.
64 | */
65 | x?: number
66 | /**
67 | * Default is the current mouse cursor position. Must be declared if `x` is
68 | * declared.
69 | */
70 | y?: number
71 | /**
72 | * The index of the menu item to be positioned under the mouse cursor at the
73 | * specified coordinates. Default is -1.
74 | *
75 | * @platform darwin
76 | */
77 | positioningItem?: number
78 | }
79 |
--------------------------------------------------------------------------------
/packages/contextmenu/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/core/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### v1.1.0 (_2024-03-16_)
2 |
3 | - fix(core): support new registering preload script menthod of Electron 35 ([#4](https://github.com/alex8088/electron-uikit/issues/4))
4 |
5 | ### v1.0.0 (_2024-06-23_)
6 |
7 | -`@electron-uikit/core` init
8 |
--------------------------------------------------------------------------------
/packages/core/README.md:
--------------------------------------------------------------------------------
1 | # @electron-uikit/core
2 |
3 | Electron UI kit core. Exposes the UI Kit APIs for communicating between main and renderer processes.
4 |
5 | ## Usage
6 |
7 | ### Install
8 |
9 | ```sh
10 | npm i @electron-uikit/core
11 | ```
12 |
13 | ### Get Started
14 |
15 | You can expose it in the specified preload script:
16 |
17 | ```js
18 | import { exposeUIKit } from '@electron-uikit/core/preload'
19 |
20 | exposeUIKit()
21 | ```
22 |
23 | Or, you can expose it globally in the main process for all renderer processes:
24 |
25 | ```js
26 | import { useUIKit } from '@electron-uikit/core/main'
27 |
28 | useUIKit()
29 | ```
30 |
31 | > [!NOTE]
32 | > If you are using [@electron-toolkit/preload](https://github.com/alex8088/electron-toolkit/tree/master/packages/preload) to expose Electron APIs, there is no need to use this module, because `core` is also an export of it.
33 |
--------------------------------------------------------------------------------
/packages/core/globals.d.ts:
--------------------------------------------------------------------------------
1 | import { UIKitAPI } from '.'
2 |
3 | declare global {
4 | interface Window {
5 | uikit: UIKitAPI
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@electron-uikit/core",
3 | "version": "1.1.0",
4 | "description": "Electron UI kit core.",
5 | "main": "dist/main.cjs",
6 | "module": "dist/main.mjs",
7 | "types": "dist/main.d.ts",
8 | "exports": {
9 | ".": {
10 | "types": "./dist/main.d.ts",
11 | "import": "./dist/main.mjs",
12 | "require": "./dist/main.cjs"
13 | },
14 | "./main": {
15 | "types": "./dist/main.d.ts",
16 | "import": "./dist/main.mjs",
17 | "require": "./dist/main.cjs"
18 | },
19 | "./preload": {
20 | "types": "./dist/preload.d.ts",
21 | "import": "./dist/preload.mjs",
22 | "require": "./dist/preload.cjs"
23 | },
24 | "./globals": {
25 | "types": "./globals.d.ts"
26 | }
27 | },
28 | "typesVersions": {
29 | "*": {
30 | "main": [
31 | "./dist/main.d.ts"
32 | ],
33 | "preload": [
34 | "./dist/preload.d.ts"
35 | ]
36 | }
37 | },
38 | "files": [
39 | "dist",
40 | "globals.d.ts"
41 | ],
42 | "author": "Alex Wei",
43 | "license": "MIT",
44 | "repository": {
45 | "type": "git",
46 | "url": "git+https://github.com/alex8088/electron-uikit.git",
47 | "directory": "packages/core"
48 | },
49 | "bugs": {
50 | "url": "https://github.com/alex8088/electron-uikit/issues"
51 | },
52 | "homepage": "https://github.com/alex8088/electron-uikit/tree/master/packages/core#readme",
53 | "keywords": [
54 | "electron",
55 | "ui",
56 | "titlebar",
57 | "contextmenu",
58 | "notification"
59 | ],
60 | "scripts": {
61 | "build": "rollup -c rollup.config.ts --configPlugin typescript"
62 | },
63 | "peerDependencies": {
64 | "electron": ">=15.0.0"
65 | },
66 | "devDependencies": {
67 | "@electron-toolkit/preload": "^3.0.1"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/packages/core/rollup.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */
2 | import { defineConfig } from 'rollup'
3 | import resolve from '@rollup/plugin-node-resolve'
4 | import commonjs from '@rollup/plugin-commonjs'
5 | import ts from '@rollup/plugin-typescript'
6 | import dts from 'rollup-plugin-dts'
7 | import rm from 'rollup-plugin-rm'
8 |
9 | export default defineConfig([
10 | {
11 | input: ['src/global.ts'],
12 | output: [
13 | {
14 | entryFileNames: 'uikit-preload.cjs',
15 | format: 'cjs',
16 | dir: 'dist'
17 | },
18 | {
19 | entryFileNames: 'uikit-preload.mjs',
20 | format: 'es',
21 | dir: 'dist'
22 | }
23 | ],
24 | external: ['electron'],
25 | plugins: [rm('dist', 'buildStart'), resolve(), commonjs()]
26 | },
27 | {
28 | input: ['src/main.ts', 'src/preload.ts'],
29 | output: [
30 | {
31 | entryFileNames: '[name].cjs',
32 | chunkFileNames: 'chunks/lib-[hash].cjs',
33 | format: 'cjs',
34 | dir: 'dist'
35 | },
36 | {
37 | entryFileNames: '[name].mjs',
38 | chunkFileNames: 'chunks/lib-[hash].mjs',
39 | format: 'es',
40 | dir: 'dist'
41 | }
42 | ],
43 | external: ['electron'],
44 | plugins: [
45 | resolve(),
46 | commonjs(),
47 | ts({
48 | compilerOptions: {
49 | rootDir: 'src',
50 | declaration: true,
51 | outDir: 'dist/types'
52 | }
53 | }),
54 | {
55 | name: 'import-meta-url',
56 | resolveImportMeta(property, { format }) {
57 | if (property === 'url' && format === 'cjs') {
58 | return `require("url").pathToFileURL(__filename).href`
59 | }
60 | return null
61 | }
62 | }
63 | ]
64 | },
65 | {
66 | input: ['dist/types/main.d.ts'],
67 | output: [{ file: './dist/main.d.ts', format: 'es' }],
68 | plugins: [dts({ respectExternal: true })],
69 | external: ['electron']
70 | },
71 | {
72 | input: ['dist/types/preload.d.ts'],
73 | output: [{ file: './dist/preload.d.ts', format: 'es' }],
74 | plugins: [dts(), rm('dist/types', 'buildEnd')]
75 | }
76 | ])
77 |
--------------------------------------------------------------------------------
/packages/core/src/global.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge } from 'electron'
2 | import { electronAPI } from '@electron-toolkit/preload'
3 |
4 | if (process.contextIsolated) {
5 | try {
6 | contextBridge.exposeInMainWorld('uikit', electronAPI)
7 | } catch (error) {
8 | console.error(error)
9 | }
10 | } else {
11 | // @ts-ignore (need dts)
12 | window.uikit = electronAPI
13 | }
14 |
--------------------------------------------------------------------------------
/packages/core/src/main.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url'
2 | import { session as _session } from 'electron'
3 |
4 | export type {
5 | IpcRenderer,
6 | NodeProcess,
7 | WebFrame,
8 | ElectronAPI as UIKitAPI
9 | } from '@electron-toolkit/preload'
10 |
11 | type UIKitOptions = {
12 | /**
13 | * Attach ES module preload script, Default: false
14 | */
15 | esModule: boolean
16 | }
17 |
18 | /**
19 | * Use UI kit for the specified session.
20 | */
21 | export function useUIKit(
22 | session = _session.defaultSession,
23 | options: UIKitOptions = { esModule: false }
24 | ): void {
25 | const electronVer = process.versions.electron
26 | const electronMajorVer = electronVer ? parseInt(electronVer.split('.')[0]) : 0
27 |
28 | const preloadPath = fileURLToPath(
29 | new URL(
30 | options.esModule ? 'uikit-preload.mjs' : 'uikit-preload.cjs',
31 | import.meta.url
32 | )
33 | )
34 |
35 | if (electronMajorVer >= 35) {
36 | session.registerPreloadScript({ type: 'frame', filePath: preloadPath })
37 | } else {
38 | session.setPreloads([...session.getPreloads(), preloadPath])
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/core/src/preload.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge } from 'electron'
2 | import { electronAPI } from '@electron-toolkit/preload'
3 |
4 | /**
5 | * Expose UI kit in the specified preload script.
6 | */
7 | export function exposeUIKit(): void {
8 | if (process.contextIsolated) {
9 | try {
10 | contextBridge.exposeInMainWorld('uikit', electronAPI)
11 | } catch (error) {
12 | console.error(error)
13 | }
14 | } else {
15 | // @ts-ignore (need dts)
16 | window.uikit = electronAPI
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/notification/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### v1.0.0 (_2024-06-23_)
2 |
3 | -`@electron-uikit/notification` init
4 |
--------------------------------------------------------------------------------
/packages/notification/README.md:
--------------------------------------------------------------------------------
1 | # @electron-uikit/notification
2 |
3 | 
4 |
5 | Notification for Electron app.
6 |
7 | Windows notifications are not very nice and friendly, which is the motivation for designing this library. In Windows, notifications based on the implementation of the Electron window are used. In other platforms such as macOS, Electron notifications will still be used, so it can be used on any platform.
8 |
9 | | Default | Customization |
10 | | :-----------------------------------: | :-----------------------------------------------: |
11 | |  |  |
12 |
13 | ## Features
14 |
15 | - MacOS notifications styles
16 | - Keep only one window and automatically update notifications
17 | - Supports hover stay
18 | - Supports dark mode
19 | - Fully customizable
20 | - Compatible with Windows, MacOS and Linux.
21 |
22 | ## Usage
23 |
24 | ### Install
25 |
26 | ```sh
27 | npm i @electron-uikit/core @electron-uikit/notification
28 | ```
29 |
30 | ### Get Started
31 |
32 | #### Using the notification in the main process.
33 |
34 | ```js
35 | import { join } from 'path'
36 | import { notification } from '@electron-uikit/notification'
37 |
38 | notification.config({
39 | /**
40 | * Time in milliseconds before notification is closed and
41 | * should be greater then 2000. Defaults to 5000.
42 | */
43 | duration: 10000,
44 | /**
45 | * Default icon for the notification.
46 | */
47 | icon: join(__dirname, '../.../resources/icon.png')
48 | //...
49 | })
50 |
51 | // Only supports listening in the main process
52 | notification.on('click', (data) => {
53 | if (data.type === 1) {
54 | //...
55 | }
56 | if (data.type === 2) {
57 | //...
58 | }
59 | // ...
60 | })
61 |
62 | notification.show({
63 | title: 'Electron UIKit',
64 | body: 'Gorgeous',
65 | extraData: { type: 1 } // click event data
66 | })
67 | ```
68 |
69 | #### Using the notification in the renderer process.
70 |
71 | 1. Exposes the UI Kit APIs for components. See [@electron-uikit/core](https://github.com/alex8088/electron-uikit/tree/main/packages/core) guide for more details.
72 |
73 | You can expose it in the specified preload script:
74 |
75 | ```js
76 | import { exposeUIKit } from '@electron-uikit/core/preload'
77 |
78 | exposeUIKit()
79 | ```
80 |
81 | Or, you can expose it globally in the main process for all renderer processes:
82 |
83 | ```js
84 | import { useUIKit } from '@electron-uikit/core/main'
85 |
86 | useUIKit()
87 | ```
88 |
89 | > [!NOTE]
90 | > If you are using [@electron-toolkit/preload](https://github.com/alex8088/electron-toolkit/tree/master/packages/preload) to expose Electron APIs, there is no need to use this module, because `core` is also an export of it.
91 |
92 | 2. Register a listener in the main process, so that you can use it in the renderer process.
93 |
94 | ```js
95 | import { app } from 'electron'
96 | import { registerNotificationListener } from '@electron-uikit/notification'
97 |
98 | app.whenReady().then(() => {
99 | // Register notification IPC listeners
100 | registerNotificationListener()
101 |
102 | // ...
103 | })
104 | ```
105 |
106 | 3. Use the notification in the renderer process.
107 |
108 | ```js
109 | import { notification } from '@electron-uikit/notification/renderer'
110 |
111 | notification.show({
112 | title: 'Electron UIKit',
113 | body: '🎉🌈'
114 | })
115 | ```
116 |
117 | ## APIs
118 |
119 | ### `.config(options)`
120 |
121 | Configure notification defaults or customize notification windows. Can only be used in the main process.
122 |
123 | - options: `object`
124 |
125 | - **title**: `string` (optional) - Default title for the notification. It can also be overridden by the title option of the show method. Defaults to [app.name](https://www.electronjs.org/zh/docs/latest/api/app#appname).
126 | - **icon**: `string` | `Electron.NativeImage` (optional) - Default icon for the notification, which can be an icon path, url protocol or [Electron NativeImage](https://www.electronjs.org/zh/docs/latest/api/native-image) object. It can also be overridden by the icon option of the show method.
127 | - **offset**: `number` (optional) _Windows_ - Notification window is offset from the bottom and right side.
128 | - **duration**: `number` (optional) _Windows_ - Time in milliseconds before notification is closed and should be greater then 2000. Defaults to 5000.
129 | - **customPage**:`string` (optional) _Windows_ - Custom html page loaded by notification window. It should be an absolute path.
130 | - **width**: `number` (optional) _Windows_ - Custom notification window width. Only valid if `customPage` has a value.
131 | - **height**: `number` (optional) _Windows_ - Custom notification window height. Only valid if `customPage` has a value.
132 | - **debug**: `boolean` (optional) _Windows_ - When set to `true`, it will open the devTool for debugging custom notification window. You should not enable this in production.
133 |
134 | ### `.show(info)`
135 |
136 | Immediately shows the notification to the user. On Windows, if the notification has been shown before, this method will re-render the notification window and continue to display it. For other platforms, see [Electron's Notification Guide](https://www.electronjs.org/docs/latest/api/notification) for more details.
137 |
138 | - info: `object`
139 |
140 | - **title**: `string` (optional) - A title for the notification, which will be shown at the top of the notification window when it is shown.
141 | - **body**: `string` (optional) - The body text of the notification, which will be displayed below the title.
142 | - **icon**: `string` | `Electron.NativeImage` (optional) - An icon to use in the notification. NativeImage icon is not supported in the renderer process.
143 | - **extraData**: `any` (optional) - Extra data for click events.
144 |
145 | ### `.destroy()`
146 |
147 | Dismiss the notification window. Available for Windows only.
148 |
149 | ### `.on(event, cb)`
150 |
151 | Listen to a notification event. Can only be used in the main process.
152 |
153 | The following notification events are dispatched:
154 |
155 | - `'click'` emitted when the notification is clicked by the user. If the notification is showed without the `extraData` option, this event will not be emitted. The event callback will receive the `extraData` option when emitted. You can customize `extraData` options to define click event handling.
156 |
157 | ```ts
158 | // Only supports listening in the main process
159 | notification.on('click', (data) => {
160 | if (data.type === 1) {
161 | //...
162 | }
163 | if (data.type === 2) {
164 | //...
165 | }
166 | // ...
167 | })
168 | ```
169 |
170 | ## Customization
171 |
172 | Customization is only available on Windows. You can use HTML to customize your own notification window UI.
173 |
174 | 1. Customize window UI.
175 |
176 | ```html
177 |
178 |
179 |
180 |
181 |
185 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
211 |
212 | ```
213 |
214 | 2. Use and debug custom window HTML page.
215 |
216 | ```js
217 | import { join } from 'path'
218 |
219 | notification.config({
220 | duration: 1000000, // Set a long duration for debugging
221 | customPage: join(__dirname, 'index.html'),
222 | height: 300,
223 | width: 300,
224 | debug: true // Open the devTool for debugging
225 | })
226 | ```
227 |
--------------------------------------------------------------------------------
/packages/notification/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@electron-uikit/notification",
3 | "version": "1.0.0",
4 | "description": "Notification for Electron app.",
5 | "main": "dist/main.cjs",
6 | "module": "dist/main.mjs",
7 | "types": "dist/main.d.ts",
8 | "exports": {
9 | ".": {
10 | "types": "./dist/main.d.ts",
11 | "import": "./dist/main.mjs",
12 | "require": "./dist/main.cjs"
13 | },
14 | "./main": {
15 | "types": "./dist/main.d.ts",
16 | "import": "./dist/main.mjs",
17 | "require": "./dist/main.cjs"
18 | },
19 | "./renderer": {
20 | "types": "./dist/renderer.d.ts",
21 | "import": "./dist/renderer.mjs"
22 | }
23 | },
24 | "typesVersions": {
25 | "*": {
26 | "main": [
27 | "./dist/main.d.ts"
28 | ],
29 | "renderer": [
30 | "./dist/renderer.d.ts"
31 | ]
32 | }
33 | },
34 | "files": [
35 | "dist"
36 | ],
37 | "author": "Alex Wei",
38 | "license": "MIT",
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/alex8088/electron-uikit.git",
42 | "directory": "packages/notification"
43 | },
44 | "bugs": {
45 | "url": "https://github.com/alex8088/electron-uikit/issues"
46 | },
47 | "homepage": "https://github.com/alex8088/electron-uikit/tree/master/packages/notification#readme",
48 | "keywords": [
49 | "electron",
50 | "notification"
51 | ],
52 | "scripts": {
53 | "build": "rollup -c rollup.config.ts --configPlugin typescript"
54 | },
55 | "peerDependencies": {
56 | "@electron-uikit/core": "*",
57 | "electron": ">=15.0.0"
58 | },
59 | "peerDependenciesMeta": {
60 | "@electron-uikit/core": {
61 | "optional": true
62 | }
63 | },
64 | "dependencies": {
65 | "@tiny-libs/typed-event-emitter": "^1.0.0"
66 | },
67 | "devDependencies": {
68 | "@electron-uikit/core": "workspace:^"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/packages/notification/rollup.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */
2 | import fs from 'node:fs/promises'
3 | import path from 'node:path'
4 | import { defineConfig } from 'rollup'
5 | import resolve from '@rollup/plugin-node-resolve'
6 | import commonjs from '@rollup/plugin-commonjs'
7 | import ts from '@rollup/plugin-typescript'
8 | import dts from 'rollup-plugin-dts'
9 | import rm from 'rollup-plugin-rm'
10 |
11 | export default defineConfig([
12 | {
13 | input: ['src/main.ts'],
14 | output: [
15 | {
16 | entryFileNames: '[name].cjs',
17 | chunkFileNames: 'chunks/lib-[hash].cjs',
18 | format: 'cjs',
19 | dir: 'dist'
20 | },
21 | {
22 | entryFileNames: '[name].mjs',
23 | chunkFileNames: 'chunks/lib-[hash].mjs',
24 | format: 'es',
25 | dir: 'dist'
26 | }
27 | ],
28 | external: ['electron', '@tiny-libs/typed-event-emitter'],
29 | plugins: [
30 | resolve(),
31 | commonjs(),
32 | ts({
33 | compilerOptions: {
34 | rootDir: 'src',
35 | declaration: true,
36 | outDir: 'dist/types'
37 | }
38 | }),
39 | rm('dist', 'buildStart'),
40 | {
41 | name: 'import-meta-url',
42 | resolveImportMeta(property, { format }) {
43 | if (property === 'url' && format === 'cjs') {
44 | return `require("url").pathToFileURL(__filename).href`
45 | }
46 | return null
47 | }
48 | }
49 | ]
50 | },
51 | {
52 | input: ['src/preload.ts'],
53 | output: [
54 | {
55 | entryFileNames: '[name].js',
56 | chunkFileNames: 'chunks/lib-[hash].js',
57 | format: 'cjs',
58 | dir: 'dist'
59 | }
60 | ],
61 | external: ['electron'],
62 | plugins: [
63 | resolve(),
64 | commonjs(),
65 | ts(),
66 | {
67 | name: 'copy-html',
68 | buildEnd: () => {
69 | fs.copyFile(
70 | path.resolve('src/index.html'),
71 | path.resolve('dist/index.html')
72 | )
73 | fs.copyFile(
74 | path.resolve('src/icon.svg'),
75 | path.resolve('dist/icon.svg')
76 | )
77 | }
78 | }
79 | ]
80 | },
81 | {
82 | input: ['src/renderer.ts'],
83 | output: [
84 | { file: './dist/renderer.mjs', format: 'es' },
85 | { name: 'renderer', file: './dist/renderer.js', format: 'iife' }
86 | ],
87 | external: ['electron'],
88 | plugins: [
89 | ts({
90 | compilerOptions: {
91 | rootDir: 'src',
92 | declaration: true,
93 | outDir: 'dist/types'
94 | }
95 | })
96 | ]
97 | },
98 | {
99 | input: ['dist/types/main.d.ts'],
100 | output: [{ file: './dist/main.d.ts', format: 'es' }],
101 | plugins: [dts()]
102 | },
103 | {
104 | input: ['dist/types/renderer.d.ts'],
105 | output: [{ file: './dist/renderer.d.ts', format: 'es' }],
106 | plugins: [dts(), rm('dist/types', 'buildEnd')]
107 | }
108 | ])
109 |
--------------------------------------------------------------------------------
/packages/notification/screenshots/customization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex8088/electron-uikit/37a35a1ead8d8018e1581ebbf7c7336f199ec1b2/packages/notification/screenshots/customization.png
--------------------------------------------------------------------------------
/packages/notification/screenshots/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex8088/electron-uikit/37a35a1ead8d8018e1581ebbf7c7336f199ec1b2/packages/notification/screenshots/default.png
--------------------------------------------------------------------------------
/packages/notification/src/common.ts:
--------------------------------------------------------------------------------
1 | import type { UIKitAPI } from '@electron-uikit/core'
2 |
3 | export const core = ((globalThis || window).uikit ||
4 | (globalThis || window).electron) as UIKitAPI
5 |
--------------------------------------------------------------------------------
/packages/notification/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const NOTIFICATION_WIDTH = 300
2 | export const NOTIFICATION_HEIGHT = 64
3 | export const NOTIFICATION_OFFSET = 6
4 | export const NOTIFICATION_DURATION = 5000
5 | export const NOTIFICATION_CHANNEL = 'uikit:notification'
6 | export const NOTIFICATION_SHOW_CHANNEL = 'uikit:notification:show'
7 | export const NOTIFICATION_CLOSE_CHANNEL = 'uikit:notification:close'
8 | export const NOTIFICATION_ON_CLICK_CHANNEL = 'uikit:notification:on-click'
9 |
--------------------------------------------------------------------------------
/packages/notification/src/icon.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/packages/notification/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Notifications
6 |
10 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/packages/notification/src/main.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { fileURLToPath } from 'node:url'
3 | import {
4 | app,
5 | ipcMain,
6 | BrowserWindow,
7 | screen,
8 | Notification as ElectronNotification,
9 | type IpcMainEvent
10 | } from 'electron'
11 | import { TypedEventEmitter } from '@tiny-libs/typed-event-emitter'
12 |
13 | import {
14 | NOTIFICATION_WIDTH,
15 | NOTIFICATION_HEIGHT,
16 | NOTIFICATION_OFFSET,
17 | NOTIFICATION_DURATION,
18 | NOTIFICATION_CHANNEL,
19 | NOTIFICATION_SHOW_CHANNEL,
20 | NOTIFICATION_CLOSE_CHANNEL,
21 | NOTIFICATION_ON_CLICK_CHANNEL
22 | } from './constants'
23 |
24 | import type {
25 | NotificationOptions,
26 | NotificationInfo,
27 | SerializableNotificationInfo
28 | } from './types'
29 |
30 | export type { NotificationOptions, NotificationInfo } from './types'
31 |
32 | type NotificationEvents = {
33 | click: (extraData: any) => void
34 | }
35 |
36 | class Notification extends TypedEventEmitter {
37 | private title: string
38 | private icon: string
39 |
40 | private offset = NOTIFICATION_OFFSET
41 | private duration = NOTIFICATION_DURATION
42 |
43 | private customPage?: string
44 | private width = NOTIFICATION_WIDTH
45 | private height = NOTIFICATION_HEIGHT
46 |
47 | private debug = false
48 |
49 | private window?: BrowserWindow
50 |
51 | constructor() {
52 | super()
53 | this.title = app.name
54 | this.icon = fileURLToPath(new URL('icon.svg', import.meta.url))
55 | }
56 |
57 | private createWindow(info: SerializableNotificationInfo): void {
58 | const { x, y, width, height } = screen.getPrimaryDisplay().workArea
59 |
60 | const win = new BrowserWindow({
61 | title: 'Notifications',
62 | width: this.width,
63 | height: this.height,
64 | x: x + width - this.width - this.offset,
65 | y: y + height - this.height - this.offset,
66 | show: false,
67 | frame: false,
68 | titleBarStyle: 'hidden',
69 | fullscreenable: false,
70 | minimizable: false,
71 | maximizable: false,
72 | movable: false,
73 | resizable: false,
74 | alwaysOnTop: true,
75 | skipTaskbar: true,
76 | webPreferences: {
77 | preload: fileURLToPath(new URL('preload.js', import.meta.url)),
78 | backgroundThrottling: false,
79 | enableWebSQL: false,
80 | spellcheck: false,
81 | devTools: this.debug && !!this.customPage
82 | }
83 | })
84 |
85 | win.setAlwaysOnTop(true, 'screen-saver')
86 |
87 | win.on('ready-to-show', () => {
88 | win.webContents.send(NOTIFICATION_SHOW_CHANNEL, info)
89 | win.showInactive()
90 | if (this.debug && !!this.customPage) win.webContents.openDevTools()
91 | })
92 |
93 | const onClick = (event: IpcMainEvent, extraData): void => {
94 | this.emit('click', extraData)
95 | const win = BrowserWindow.fromWebContents(event.sender)
96 | if (win && !win.isDestroyed()) {
97 | win.close()
98 | }
99 | }
100 |
101 | const onClose = (event: IpcMainEvent, immediately: boolean): void => {
102 | const win = BrowserWindow.fromWebContents(event.sender)
103 | if (win) {
104 | if (!immediately && this.isMouseOver(win)) {
105 | event.reply(NOTIFICATION_SHOW_CHANNEL)
106 | } else {
107 | win.close()
108 | }
109 | }
110 | }
111 |
112 | win.on('closed', () => {
113 | ipcMain.removeListener(NOTIFICATION_ON_CLICK_CHANNEL, onClick)
114 | ipcMain.removeListener(NOTIFICATION_CLOSE_CHANNEL, onClose)
115 |
116 | this.window = undefined
117 | })
118 |
119 | ipcMain.on(NOTIFICATION_ON_CLICK_CHANNEL, onClick)
120 | ipcMain.on(NOTIFICATION_CLOSE_CHANNEL, onClose)
121 |
122 | win.loadFile(
123 | this.customPage || fileURLToPath(new URL('index.html', import.meta.url))
124 | )
125 |
126 | this.window = win
127 | }
128 |
129 | private isMouseOver(window: BrowserWindow): boolean {
130 | const screenCoord = screen.getCursorScreenPoint()
131 | const winBounds = window.getBounds()
132 | return (
133 | screenCoord.x >= winBounds.x &&
134 | screenCoord.x <= winBounds.x + winBounds.width &&
135 | screenCoord.y >= winBounds.y &&
136 | screenCoord.y <= winBounds.y + winBounds.height
137 | )
138 | }
139 |
140 | /**
141 | * Configure notification defaults or customize notification windows.
142 | * @param options NotificationOptions
143 | */
144 | config(options: NotificationOptions): void {
145 | const {
146 | title = app.name,
147 | icon,
148 | offset = NOTIFICATION_OFFSET,
149 | duration = NOTIFICATION_DURATION,
150 | customPage,
151 | width = NOTIFICATION_WIDTH,
152 | height = NOTIFICATION_HEIGHT,
153 | debug = false
154 | } = options
155 |
156 | this.title = title
157 | this.icon = icon
158 | ? typeof icon === 'object'
159 | ? icon.toDataURL()
160 | : icon
161 | : this.icon
162 |
163 | this.offset = offset
164 | this.duration = Math.max(duration, 2000)
165 |
166 | if (customPage) {
167 | this.customPage = customPage
168 | this.width = width
169 | this.height = height
170 | this.debug = debug
171 | }
172 | }
173 |
174 | /**
175 | * Dismiss the notification windows.
176 | *
177 | * @platform win32
178 | */
179 | destroy(): void {
180 | if (this.window) {
181 | this.window.close()
182 | }
183 | }
184 |
185 | /**
186 | * Immediately shows the notification to the user. On Windows, if the notification
187 | * has been shown before, this method will re-render the notification window and
188 | * continue to display it. For other platforms,
189 | * see [Electron's Notification Guide](https://www.electronjs.org/docs/latest/api/notification) for more details.
190 | *
191 | * @param info NotificationInfo
192 | */
193 | show(info: NotificationInfo): void {
194 | const { title, body, icon, extraData } = info
195 |
196 | const _info: SerializableNotificationInfo = {
197 | title: title || this.title,
198 | body,
199 | icon: icon
200 | ? typeof icon === 'object'
201 | ? icon.toDataURL()
202 | : icon
203 | : this.icon,
204 | extraData,
205 | duration: this.duration,
206 | custom: !!this.customPage
207 | }
208 |
209 | if (process.platform === 'win32') {
210 | if (this.window && !this.window.isDestroyed()) {
211 | this.window.webContents.send(NOTIFICATION_SHOW_CHANNEL, _info)
212 |
213 | if (!this.window.isVisible()) {
214 | this.window.showInactive()
215 | }
216 | } else {
217 | this.createWindow(_info)
218 | }
219 | } else {
220 | if (!ElectronNotification.isSupported()) {
221 | return
222 | }
223 |
224 | const { title = this.title, body, icon, extraData } = info
225 |
226 | const notifier = new ElectronNotification({ title, body, icon })
227 | notifier.on('click', () => {
228 | this.emit('click', extraData)
229 | })
230 | notifier.show()
231 | }
232 | }
233 | }
234 |
235 | export const notification = new Notification()
236 |
237 | /**
238 | * Register the notification ipc listener for use by the renderer.
239 | */
240 | export function registerNotificationListener(): void {
241 | if (ipcMain.eventNames().some((ename) => ename === NOTIFICATION_CHANNEL)) {
242 | return
243 | }
244 |
245 | ipcMain.on(NOTIFICATION_CHANNEL, (_, info?: NotificationInfo) => {
246 | if (info) {
247 | notification.show(info)
248 | } else {
249 | notification.destroy()
250 | }
251 | })
252 | }
253 |
--------------------------------------------------------------------------------
/packages/notification/src/preload.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | import { ipcRenderer, contextBridge } from 'electron'
4 | import {
5 | NOTIFICATION_SHOW_CHANNEL,
6 | NOTIFICATION_CLOSE_CHANNEL,
7 | NOTIFICATION_ON_CLICK_CHANNEL
8 | } from './constants'
9 |
10 | import type { SerializableNotificationInfo } from './types'
11 |
12 | let timeout: NodeJS.Timeout | null = null
13 |
14 | ipcRenderer.on(
15 | NOTIFICATION_SHOW_CHANNEL,
16 | (_, data?: SerializableNotificationInfo) => {
17 | let ms
18 | if (data) {
19 | ms = data.duration
20 | if (data.custom) {
21 | const { title, body, icon, extraData } = data
22 | window.dispatchEvent(
23 | new CustomEvent('notification-message', {
24 | detail: { title, body, icon, extraData }
25 | })
26 | )
27 | } else {
28 | h(data.title, data.body, data.icon, data.extraData)
29 | }
30 | } else {
31 | ms = 2000
32 | }
33 | if (timeout) clearTimeout(timeout)
34 | timeout = setTimeout(() => {
35 | ipcRenderer.send(NOTIFICATION_CLOSE_CHANNEL, false)
36 | }, ms)
37 | }
38 | )
39 |
40 | contextBridge.exposeInMainWorld('notification', {
41 | close: () => {
42 | ipcRenderer.send(NOTIFICATION_CLOSE_CHANNEL, true)
43 | },
44 | doClick: (extraData: any) => {
45 | ipcRenderer.send(NOTIFICATION_ON_CLICK_CHANNEL, extraData)
46 | }
47 | })
48 |
49 | function h(
50 | title: string,
51 | subtitle?: string,
52 | icon?: string,
53 | extraData?: any
54 | ): void {
55 | while (document.body.firstChild) {
56 | document.body.firstChild.remove()
57 | }
58 |
59 | const container = document.createElement('div')
60 | container.classList.add('n')
61 |
62 | if (extraData) {
63 | container.addEventListener('click', () => {
64 | ipcRenderer.send(NOTIFICATION_ON_CLICK_CHANNEL, extraData)
65 | })
66 | }
67 |
68 | if (icon) {
69 | const header = document.createElement('div')
70 | header.classList.add('hd')
71 | const img = document.createElement('img')
72 | img.src = icon
73 | img.alt = ''
74 | header.appendChild(img)
75 |
76 | container.appendChild(header)
77 | }
78 |
79 | const body = document.createElement('div')
80 | body.classList.add('bd')
81 |
82 | const h4 = document.createElement('h4')
83 | h4.innerText = title
84 | body.appendChild(h4)
85 |
86 | if (subtitle) {
87 | const p = document.createElement('p')
88 | p.innerText = subtitle
89 | body.appendChild(p)
90 | }
91 |
92 | container.appendChild(body)
93 |
94 | document.body.appendChild(container)
95 |
96 | const close = document.createElement('div')
97 | close.classList.add('close')
98 |
99 | close.addEventListener('click', () => {
100 | ipcRenderer.send(NOTIFICATION_CLOSE_CHANNEL, true)
101 | })
102 |
103 | document.body.appendChild(close)
104 | }
105 |
--------------------------------------------------------------------------------
/packages/notification/src/renderer.ts:
--------------------------------------------------------------------------------
1 | import { core } from './common'
2 | import { NOTIFICATION_CHANNEL } from './constants'
3 |
4 | import type { NotificationInfo } from './types'
5 |
6 | interface NotificationIcon {
7 | /**
8 | * An icon to use in the notification.
9 | */
10 | icon?: string
11 | }
12 |
13 | export const notification = {
14 | show: (info: Omit & NotificationIcon): void => {
15 | core.ipcRenderer.send(NOTIFICATION_CHANNEL, info)
16 | },
17 | destroy: (): void => {
18 | core.ipcRenderer.send(NOTIFICATION_CHANNEL)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/notification/src/types.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | import { NativeImage } from 'electron'
4 |
5 | export type NotificationOptions = {
6 | /**
7 | * Default title for the notification. It can also be overridden by the
8 | * title option of the show method. Defaults to
9 | * [`app.name`](https://www.electronjs.org/zh/docs/latest/api/app#appname).
10 | */
11 | title?: string
12 | /**
13 | * Default icon for the notification, which can be an icon path, url protocol
14 | * or [Electron NativeImage](https://www.electronjs.org/zh/docs/latest/api/native-image) object.
15 | * It can also be overridden by the icon option of the show method.
16 | */
17 | icon?: string | NativeImage
18 | /**
19 | * Notification window is offset from the bottom and right side.
20 | *
21 | * @platform win32
22 | */
23 | offset?: number
24 | /**
25 | * Time in milliseconds before notification is closed and should be greater then `2000`.
26 | * Defaults to `5000`.
27 | *
28 | * @platform win32
29 | */
30 | duration?: number
31 | /**
32 | * Custom html page loaded by notification window. It should be an absolute path.
33 | *
34 | * @platform win32
35 | */
36 | customPage?: string
37 | /**
38 | * Custom notification window width. Only valid if `customPage` has a value.
39 | *
40 | * @platform win32
41 | */
42 | width?: number
43 | /**
44 | * Custom notification window height. Only valid if `customPage` has a value.
45 | *
46 | * @platform win32
47 | */
48 | height?: number
49 | /**
50 | * When set to `true`, it will open the devTool for debugging custom notification
51 | * window. You should not enable this in production.
52 | *
53 | * @platform win32
54 | */
55 | debug?: boolean
56 | }
57 |
58 | export interface NotificationInfo {
59 | /**
60 | * A title for the notification, which will be shown at the top of the notification
61 | * window when it is shown.
62 | */
63 | title?: string
64 | /**
65 | * The body text of the notification, which will be displayed below the title.
66 | */
67 | body?: string
68 | /**
69 | * An icon to use in the notification.
70 | */
71 | icon?: string | NativeImage
72 | /**
73 | * Extra data for click events
74 | */
75 | extraData?: any
76 | }
77 |
78 | export interface SerializableNotificationInfo {
79 | title: string
80 | body?: string
81 | icon?: string
82 | extraData?: any
83 | duration: number
84 | custom: boolean
85 | }
86 |
--------------------------------------------------------------------------------
/packages/notification/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/playground/.env.preload:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | VITE_USE_PRELOAD=true
3 |
--------------------------------------------------------------------------------
/packages/playground/README.md:
--------------------------------------------------------------------------------
1 | # Playground for Electron UIKit
2 |
--------------------------------------------------------------------------------
/packages/playground/electron.vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
2 |
3 | export default defineConfig(({ mode }) => {
4 | return {
5 | main: {
6 | plugins: [externalizeDepsPlugin()]
7 | },
8 | ...(mode === 'preload' ? { preload: {} } : null),
9 | renderer: {}
10 | }
11 | })
12 |
--------------------------------------------------------------------------------
/packages/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@electron-uikit/playground",
3 | "version": "0.0.0",
4 | "private": true,
5 | "main": "out/main/index.js",
6 | "scripts": {
7 | "dev": "electron-vite dev --ignoreConfigWarning",
8 | "dev:preload": "electron-vite dev --mode preload"
9 | },
10 | "dependencies": {
11 | "@electron-uikit/contextmenu": "workspace:^",
12 | "@electron-uikit/core": "workspace:^",
13 | "@electron-uikit/notification": "workspace:^",
14 | "@electron-uikit/titlebar": "workspace:^",
15 | "@electron-uikit/toast": "workspace:^"
16 | },
17 | "devDependencies": {
18 | "electron-vite": "^2.2.0",
19 | "vite": "^5.3.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/playground/src/main/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface ImportMetaEnv {
4 | readonly VITE_USE_PRELOAD: string
5 | }
6 |
7 | interface ImportMeta {
8 | readonly env: ImportMetaEnv
9 | }
10 |
--------------------------------------------------------------------------------
/packages/playground/src/main/index.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, ipcMain, nativeTheme, shell } from 'electron'
2 | import { join } from 'path'
3 |
4 | import { useUIKit } from '@electron-uikit/core'
5 | import { registerContextMenuListener } from '@electron-uikit/contextmenu'
6 | import {
7 | notification,
8 | registerNotificationListener
9 | } from '@electron-uikit/notification'
10 | import {
11 | registerTitleBarListener,
12 | attachTitleBarToWindow
13 | } from '@electron-uikit/titlebar'
14 | import { Toast } from '@electron-uikit/toast'
15 |
16 | function createWindow(): void {
17 | const mainWindow = new BrowserWindow({
18 | title: 'Electron UIKit',
19 | width: 720,
20 | height: 520,
21 | show: false,
22 | autoHideMenuBar: true,
23 | titleBarStyle: 'hidden',
24 | webPreferences: {
25 | ...(import.meta.env.VITE_USE_PRELOAD === 'true'
26 | ? { preload: join(__dirname, '../preload/index.js') }
27 | : {}),
28 | sandbox: false
29 | }
30 | })
31 |
32 | attachTitleBarToWindow(mainWindow)
33 |
34 | mainWindow.on('ready-to-show', () => {
35 | mainWindow.show()
36 | })
37 |
38 | mainWindow.webContents.on('before-input-event', (_, input) => {
39 | if (input.type === 'keyDown') {
40 | if (input.code === 'F12') {
41 | if (mainWindow.webContents.isDevToolsOpened()) {
42 | mainWindow.webContents.closeDevTools()
43 | } else {
44 | mainWindow.webContents.openDevTools({ mode: 'undocked' })
45 | }
46 | }
47 | }
48 | })
49 |
50 | if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) {
51 | mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
52 | } else {
53 | mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
54 | }
55 | }
56 |
57 | app.whenReady().then(() => {
58 | if (import.meta.env.VITE_USE_PRELOAD !== 'true') {
59 | useUIKit()
60 | }
61 |
62 | registerTitleBarListener()
63 | registerContextMenuListener()
64 | registerNotificationListener()
65 |
66 | notification.config({
67 | title: 'Electron UIKit'
68 | })
69 |
70 | notification.on('click', (data) => {
71 | if (data.type === 1) {
72 | shell.openExternal(
73 | 'https://github.com/alex8088/electron-uikit/tree/main/packages/notification'
74 | )
75 | }
76 | })
77 |
78 | createWindow()
79 |
80 | ipcMain.handle('dark-mode:toggle', () => {
81 | if (nativeTheme.shouldUseDarkColors) {
82 | nativeTheme.themeSource = 'light'
83 | } else {
84 | nativeTheme.themeSource = 'dark'
85 | }
86 | return nativeTheme.shouldUseDarkColors
87 | })
88 |
89 | ipcMain.on('toast:loading', (e) => {
90 | const win = BrowserWindow.fromWebContents(e.sender)
91 | if (win) {
92 | const toast = new Toast(win)
93 | const reply = toast.loading('Downloading')
94 | setTimeout(() => reply.dismiss(), 3000)
95 | }
96 | })
97 |
98 | app.on('activate', function () {
99 | if (BrowserWindow.getAllWindows().length === 0) createWindow()
100 | })
101 | })
102 |
103 | app.on('window-all-closed', () => {
104 | if (process.platform !== 'darwin') {
105 | app.quit()
106 | }
107 | })
108 |
--------------------------------------------------------------------------------
/packages/playground/src/preload/index.ts:
--------------------------------------------------------------------------------
1 | import { exposeUIKit } from '@electron-uikit/core/preload'
2 |
3 | exposeUIKit()
4 |
--------------------------------------------------------------------------------
/packages/playground/src/renderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Electron UIKit
6 |
7 |
11 |
14 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | display none
100 |
101 |
102 | Electron UIKit+
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/packages/playground/src/renderer/public/moon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/playground/src/renderer/public/sun.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/playground/src/renderer/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/playground/src/renderer/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Menu, MenuItem } from '@electron-uikit/contextmenu/renderer'
2 | import { notification } from '@electron-uikit/notification/renderer'
3 | import { toast } from '@electron-uikit/toast/renderer'
4 |
5 | function init(): void {
6 | toast.config({
7 | supportMain: true,
8 | bottom: 80
9 | })
10 | window.addEventListener('DOMContentLoaded', () => {
11 | const theme = document.getElementById('theme')
12 | const icon = document.getElementById('icon')
13 | theme?.addEventListener('click', () => {
14 | window.uikit.ipcRenderer
15 | .invoke('dark-mode:toggle')
16 | .then((dark: boolean) => {
17 | if (dark) {
18 | icon?.classList.replace('icon-sun', 'icon-moon')
19 | } else {
20 | icon?.classList.replace('icon-moon', 'icon-sun')
21 | }
22 | })
23 | })
24 |
25 | const body = document.getElementById('app')
26 | body?.addEventListener('contextmenu', (e) => {
27 | e.preventDefault()
28 |
29 | const menu = new Menu()
30 | menu.append(
31 | new MenuItem({
32 | label: 'Popup Notification',
33 | click: (): void => {
34 | notification.show({ body: 'Gorgeous', extraData: { type: 1 } })
35 | }
36 | })
37 | )
38 | menu.append(new MenuItem({ type: 'separator' }))
39 | menu.append(
40 | new MenuItem({
41 | label: 'Show Text Toast',
42 | click: (): void => {
43 | toast.text('I am toast', 4000)
44 | }
45 | })
46 | )
47 | menu.append(
48 | new MenuItem({
49 | label: 'Show Loading Toast',
50 | click: (): void => {
51 | const reply = toast.loading('Loading')
52 | setTimeout(() => reply.success('Done'), 3000000)
53 | }
54 | })
55 | )
56 | menu.append(
57 | new MenuItem({
58 | label: 'Show Loading Toast (Main Process)',
59 | click: (): void => {
60 | window.uikit.ipcRenderer.send('toast:loading')
61 | }
62 | })
63 | )
64 | menu.popup()
65 | })
66 | })
67 | }
68 |
69 | init()
70 |
--------------------------------------------------------------------------------
/packages/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/titlebar/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### v1.2.0 (_2024-12-30_)
2 |
3 | - feat: handle updates to the windowtitle attribute
4 |
5 | ### v1.1.0 (_2024-08-11_)
6 |
7 | - fix: style for overlay mode
8 |
9 | ### v1.0.0 (_2024-06-23_)
10 |
11 | - `@electron-uikit/titlebar` init
12 |
--------------------------------------------------------------------------------
/packages/titlebar/README.md:
--------------------------------------------------------------------------------
1 | # @electron-uikit/titlebar
2 |
3 | 
4 |
5 | Title bar web component for Electron app.
6 |
7 | The title bar web component is used to simulate the Windows title bar for better customization. It's very easy to use and get the same experience as a native title bar. Fully compatible with Windows, MacOS and Linux.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ## Features
18 |
19 | - Customization
20 | - Fully compatible with Windows, MacOS and Linux.
21 | - Support dark mode
22 | - Based on Web component
23 |
24 | ## Usage
25 |
26 | ### Install
27 |
28 | ```sh
29 | npm i @electron-uikit/core @electron-uikit/titlebar
30 | ```
31 |
32 | ### Get Started
33 |
34 | 1. Exposes the UI Kit APIs for components. See [@electron-uikit/core](https://github.com/alex8088/electron-uikit/tree/main/packages/core) guide for more details.
35 |
36 | You can expose it in the specified preload script:
37 |
38 | ```js
39 | import { exposeUIKit } from '@electron-uikit/core/preload'
40 |
41 | exposeUIKit()
42 | ```
43 |
44 | Or, you can expose it globally in the main process for all renderer processes:
45 |
46 | ```js
47 | import { useUIKit } from '@electron-uikit/core/main'
48 |
49 | useUIKit()
50 | ```
51 |
52 | > [!NOTE]
53 | > If you are using [@electron-toolkit/preload](https://github.com/alex8088/electron-toolkit/tree/master/packages/preload) to expose Electron APIs, there is no need to use this module, because `core` is also an export of it.
54 |
55 | 2. Register a listener and attach a title bar to the window in the main process, so that you can use it in the renderer process.
56 |
57 | ```js
58 | import { app, BrowserWindow } from 'electron'
59 | import {
60 | registerTitleBarListener,
61 | attachTitleBarToWindow
62 | } from '@electron-uikit/titlebar'
63 |
64 | app.whenReady().then(() => {
65 | // Register title bar IPC listeners
66 | registerTitleBarListener()
67 |
68 | // Create a window without title bar
69 | const win = new BrowserWindow({ titleBarStyle: 'hidden' })
70 |
71 | // Attach a title bar to the window
72 | attachTitleBarToWindow(win)
73 | })
74 | ```
75 |
76 | 3. Use the title bar web component in window html page.
77 |
78 | ```html
79 |
80 |
81 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | ```
91 |
92 | ## Components
93 |
94 | ### Attributes
95 |
96 | | Attribute | Description | Type | Default |
97 | | ------------------- | -------------------------------------------------------------------- | --------- | ------- |
98 | | `windowtitle` | Window title | `string` | - |
99 | | `overlay` | Set window controls overlay | `boolean` | false |
100 | | `nominimize` | If specified, the title bar will not show the minimization control | `boolean` | false |
101 | | `nomaximize` | If specified, the title bar will not show the nomaximization control | `boolean` | false |
102 | | `noclose` | If specified, the title bar will not show the `x` control | `boolean` | false |
103 | | `supportfullscreen` | Support fullscreen window mode | `boolean` | false |
104 |
105 | ### CSS Properties
106 |
107 | | Property | Description | Default |
108 | | --------------------------- | ----------------------------- | ----------------------------------------------------- |
109 | | `--tb-theme-color` | Theme color | light: `#ffffff`, dark: `#1f1f1f` |
110 | | `--tb-title-text-color` | Window title text color | light: `#333333`, dark: `#cccccc` |
111 | | `--tb-title-font-family` | Window title text font family | -apple-system, BlinkMacSystemFont, Ubuntu, 'Segoe UI' |
112 | | `--tb-control-symbol-color` | Window control symbol color | light: `#585c65`, dark: `#cccccc` |
113 | | `--tb-control-hover-color` | Window control hover color | light: `#e1e1e1`, dark: `#373737` |
114 | | `--tb-control-height` | Window control height | `26px` (minimum height) |
115 | | `--tb-control-width ` | Window control width | `34px` (minimum width) |
116 | | `--tb-control-margin ` | Window control margins | `3px` (MacOS only) |
117 | | `--tb-control-radius ` | Window control radius | `5px` (MacOS only) |
118 |
119 | ## Customization
120 |
121 | ### Theme Color
122 |
123 | The default theme CSS is customizable by overriding the components CSS variables:
124 |
125 | ```css
126 | .titlebar {
127 | height: 32px;
128 | --tb-theme-color: #ffffff;
129 | --tb-title-text-color: #333333;
130 | --tb-control-symbol-color: #585c65;
131 | --tb-control-hover-color: #e1e1e1;
132 | }
133 | ```
134 |
135 | Support dark mode by overriding the component CSS variable with `prefers-color-scheme`:
136 |
137 | ```css
138 | @media (prefers-color-scheme: dark) {
139 | .titlebar {
140 | --tb-theme-color: #1f1f1f;
141 | --tb-title-text-color: #cccccc;
142 | --tb-control-symbol-color: #cccccc;
143 | --tb-control-hover-color: #373737;
144 | }
145 | }
146 | ```
147 |
148 | ### Window Controls
149 |
150 | - `Overlay mode`
151 |
152 | The titlebar only has window control display and no drag region for application.
153 |
154 | ```html
155 |
156 | ```
157 |
158 | - `Control size and display`
159 |
160 | ```html
161 |
162 | ```
163 |
164 | - `Customization`
165 |
166 | It is very easy to customize a window control, just define the control icon CSS and bind event handlers to the control element.
167 |
168 | ```css
169 | /* CSS style: control icon */
170 | .icon {
171 | height: 13px;
172 | width: 13px;
173 | background-color: #333;
174 | mask-position: 50% 50%;
175 | mask-repeat: no-repeat;
176 | mask-size: 100%;
177 | }
178 |
179 | .icon-sun {
180 | mask-image: url(/sun.svg);
181 | }
182 |
183 | .icon-moon {
184 | mask-image: url(/moon.svg);
185 | }
186 | ```
187 |
188 | ```html
189 |
190 |
191 |
192 |
193 |
194 |
195 | ```
196 |
197 | ```js
198 | const control = document.getElementById('control')
199 | control?.addEventListener('click', () => {
200 | // ...
201 | })
202 | ```
203 |
204 | > [!NOTE]
205 | > Only elements with the `window__control` class in the title bar slot will be recognized as window controls. Other elements will not be displayed.
206 |
207 | ## Integrations
208 |
209 | ### VS Code IDE
210 |
211 | Add a file `.vscode/settings.json` with the following configuration:
212 |
213 | ```json
214 | {
215 | "html.customData": [
216 | "./node_modules/@electron-uikit/titlebar/custom-elements.json"
217 | ]
218 | }
219 | ```
220 |
221 | VS Code will load additional title bar HTML entities, so they would show up in auto-completion, hover information etc.
222 |
223 | ### TypeScript and TSX
224 |
225 | If you are a TypeScript user, make sure to add a \*.d.ts declaration file to get type checks and intellisense:
226 |
227 | ```ts
228 | ///
229 | ```
230 |
231 | For `.tsx` support, a React example:
232 |
233 | ```ts
234 | import { HTMLTitleBarElementAttributes } from '@electron-uikit/titlebar/renderer'
235 |
236 | declare global {
237 | namespace JSX {
238 | interface IntrinsicElements {
239 | // Define an interface for the web component props
240 | 'title-bar': Partial
241 | }
242 | }
243 | }
244 | ```
245 |
--------------------------------------------------------------------------------
/packages/titlebar/custom-elements.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1.1,
3 | "tags": [
4 | {
5 | "name": "title-bar",
6 | "description": "Title bar web component for Electron. Example:\n```html\n\n```",
7 | "references": [
8 | {
9 | "name": "Electron UIKit Reference",
10 | "url": "https://github.com/alex8088/electron-uikit"
11 | }
12 | ],
13 | "attributes": [
14 | {
15 | "name": "windowtitle",
16 | "description": "Window title."
17 | },
18 | {
19 | "name": "overlay",
20 | "description": "Set window controls overlay."
21 | },
22 | {
23 | "name": "nominimize",
24 | "description": "If specified, the title bar will not show the minimization control."
25 | },
26 | {
27 | "name": "nomaximize",
28 | "description": "If specified, the title bar will not show the nomaximization control."
29 | },
30 | {
31 | "name": "noclose",
32 | "description": "If specified, the title bar will not show the `x` control."
33 | },
34 | {
35 | "name": "supportfullscreen",
36 | "description": "Support fullscreen window mode."
37 | }
38 | ]
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/packages/titlebar/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@electron-uikit/titlebar",
3 | "version": "1.2.0",
4 | "description": "Title bar web component for Electron app.",
5 | "main": "dist/main.cjs",
6 | "module": "dist/main.mjs",
7 | "types": "dist/main.d.ts",
8 | "exports": {
9 | ".": {
10 | "types": "./dist/main.d.ts",
11 | "import": "./dist/main.mjs",
12 | "require": "./dist/main.cjs"
13 | },
14 | "./main": {
15 | "types": "./dist/main.d.ts",
16 | "import": "./dist/main.mjs",
17 | "require": "./dist/main.cjs"
18 | },
19 | "./renderer": {
20 | "types": "./dist/renderer.d.ts",
21 | "import": "./dist/renderer.mjs"
22 | }
23 | },
24 | "typesVersions": {
25 | "*": {
26 | "main": [
27 | "./dist/main.d.ts"
28 | ],
29 | "renderer": [
30 | "./dist/renderer.d.ts"
31 | ]
32 | }
33 | },
34 | "files": [
35 | "dist",
36 | "custom-elements.json"
37 | ],
38 | "author": "Alex Wei",
39 | "license": "MIT",
40 | "repository": {
41 | "type": "git",
42 | "url": "git+https://github.com/alex8088/electron-uikit.git",
43 | "directory": "packages/titlebar"
44 | },
45 | "bugs": {
46 | "url": "https://github.com/alex8088/electron-uikit/issues"
47 | },
48 | "homepage": "https://github.com/alex8088/electron-uikit/tree/master/packages/titlebar#readme",
49 | "keywords": [
50 | "electron",
51 | "titlebar"
52 | ],
53 | "scripts": {
54 | "build": "rollup -c rollup.config.ts --configPlugin typescript"
55 | },
56 | "peerDependencies": {
57 | "@electron-uikit/core": "*",
58 | "electron": ">=15.0.0"
59 | },
60 | "peerDependenciesMeta": {
61 | "@electron-uikit/core": {
62 | "optional": true
63 | }
64 | },
65 | "devDependencies": {
66 | "@electron-uikit/core": "workspace:^"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/packages/titlebar/rollup.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */
2 | import { defineConfig } from 'rollup'
3 | import resolve from '@rollup/plugin-node-resolve'
4 | import commonjs from '@rollup/plugin-commonjs'
5 | import ts from '@rollup/plugin-typescript'
6 | import dts from 'rollup-plugin-dts'
7 | import rm from 'rollup-plugin-rm'
8 |
9 | export default defineConfig([
10 | {
11 | input: ['src/main.ts'],
12 | output: [
13 | {
14 | entryFileNames: '[name].cjs',
15 | chunkFileNames: 'chunks/lib-[hash].cjs',
16 | format: 'cjs',
17 | dir: 'dist'
18 | },
19 | {
20 | entryFileNames: '[name].mjs',
21 | chunkFileNames: 'chunks/lib-[hash].mjs',
22 | format: 'es',
23 | dir: 'dist'
24 | }
25 | ],
26 | external: ['electron'],
27 | plugins: [
28 | resolve(),
29 | commonjs(),
30 | ts({
31 | compilerOptions: {
32 | rootDir: 'src',
33 | declaration: true,
34 | outDir: 'dist/types'
35 | }
36 | }),
37 | rm('dist', 'buildStart')
38 | ]
39 | },
40 | {
41 | input: ['src/renderer.ts'],
42 | output: [
43 | { file: './dist/renderer.mjs', format: 'es' },
44 | { name: 'renderer', file: './dist/renderer.js', format: 'iife' }
45 | ],
46 | plugins: [
47 | ts({
48 | compilerOptions: {
49 | rootDir: 'src',
50 | declaration: true,
51 | outDir: 'dist/types'
52 | }
53 | })
54 | ]
55 | },
56 | {
57 | input: ['dist/types/main.d.ts'],
58 | output: [{ file: './dist/main.d.ts', format: 'es' }],
59 | plugins: [dts()],
60 | external: ['electron']
61 | },
62 | {
63 | input: ['dist/types/renderer.d.ts'],
64 | output: [{ file: './dist/renderer.d.ts', format: 'es' }],
65 | plugins: [dts(), rm('dist/types', 'buildEnd')]
66 | }
67 | ])
68 |
--------------------------------------------------------------------------------
/packages/titlebar/screenshots/mac-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex8088/electron-uikit/37a35a1ead8d8018e1581ebbf7c7336f199ec1b2/packages/titlebar/screenshots/mac-light.png
--------------------------------------------------------------------------------
/packages/titlebar/screenshots/win32-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex8088/electron-uikit/37a35a1ead8d8018e1581ebbf7c7336f199ec1b2/packages/titlebar/screenshots/win32-dark.png
--------------------------------------------------------------------------------
/packages/titlebar/src/common.ts:
--------------------------------------------------------------------------------
1 | import type { UIKitAPI } from '@electron-uikit/core'
2 |
3 | export const core = ((globalThis || window).uikit ||
4 | (globalThis || window).electron) as UIKitAPI
5 |
--------------------------------------------------------------------------------
/packages/titlebar/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const TITLE_BAR_CHANNEL = 'uikit:titlebar'
2 | export const TITLE_BAR_MAXIMIZE_REPLY_CHANNEL = 'uikit:titlebar:maximize-reply'
3 | export const TITLE_BAR_FULLSCREEN_REPLY_CHANNEL =
4 | 'uikit:titlebar:fullscreen-reply'
5 |
--------------------------------------------------------------------------------
/packages/titlebar/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ipcMain, BrowserWindow } from 'electron'
2 | import {
3 | TITLE_BAR_CHANNEL,
4 | TITLE_BAR_MAXIMIZE_REPLY_CHANNEL,
5 | TITLE_BAR_FULLSCREEN_REPLY_CHANNEL
6 | } from './constants'
7 |
8 | const isMacintosh = process.platform === 'darwin'
9 |
10 | /**
11 | * Register the title bar ipc listener for use by the renderer.
12 | */
13 | export function registerTitleBarListener(): void {
14 | if (isMacintosh) {
15 | return
16 | }
17 |
18 | if (ipcMain.eventNames().some((ename) => ename === TITLE_BAR_CHANNEL)) {
19 | return
20 | }
21 |
22 | ipcMain.on(
23 | TITLE_BAR_CHANNEL,
24 | (
25 | e,
26 | action:
27 | | 'show'
28 | | 'showInactive'
29 | | 'minimize'
30 | | 'maximizeOrUnmaximize'
31 | | 'close'
32 | ) => {
33 | const win = BrowserWindow.fromWebContents(e.sender)
34 |
35 | if (win) {
36 | if (action === 'show') {
37 | win.show()
38 | } else if (action === 'showInactive') {
39 | win.showInactive()
40 | } else if (action === 'minimize') {
41 | win.minimize()
42 | } else if (action === 'maximizeOrUnmaximize') {
43 | const isMaximized = win.isMaximized()
44 | if (isMaximized) {
45 | win.unmaximize()
46 | } else {
47 | win.maximize()
48 | }
49 | } else if (action === 'close') {
50 | win.close()
51 | }
52 | }
53 | }
54 | )
55 | }
56 |
57 | /**
58 | * Attach a title bar to the window.
59 | * @param win BrowserWindow
60 | */
61 | export function attachTitleBarToWindow(win: BrowserWindow): void {
62 | if (win.fullScreenable) {
63 | win.on('enter-full-screen', () => {
64 | win.webContents.send(TITLE_BAR_FULLSCREEN_REPLY_CHANNEL, 1)
65 | })
66 |
67 | win.on('leave-full-screen', () => {
68 | win.webContents.send(TITLE_BAR_FULLSCREEN_REPLY_CHANNEL, 0)
69 | })
70 | }
71 |
72 | if (isMacintosh) {
73 | return
74 | }
75 |
76 | win.on('maximize', () => {
77 | win.webContents.send(TITLE_BAR_MAXIMIZE_REPLY_CHANNEL, 1)
78 | })
79 |
80 | win.on('unmaximize', () => {
81 | win.webContents.send(TITLE_BAR_MAXIMIZE_REPLY_CHANNEL, 0)
82 | })
83 | }
84 |
--------------------------------------------------------------------------------
/packages/titlebar/src/renderer.ts:
--------------------------------------------------------------------------------
1 | import { core } from './common'
2 | import {
3 | TITLE_BAR_CHANNEL,
4 | TITLE_BAR_MAXIMIZE_REPLY_CHANNEL,
5 | TITLE_BAR_FULLSCREEN_REPLY_CHANNEL
6 | } from './constants'
7 |
8 | const SHADOW_ROOT_CSS = `
9 | :host {
10 | position: relative;
11 | background-color: var(--tb-theme-color, #ffffff);
12 | -webkit-user-select: none;
13 | --tb-title-text-color: #333333;
14 | --tb-title-font-family: -apple-system, BlinkMacSystemFont, Ubuntu, 'Segoe UI';
15 | --tb-control-height: 26px;
16 | --tb-control-width: 34px;
17 | --tb-control-margin: 3px;
18 | --tb-control-radius: 5px;
19 | --tb-control-symbol-color: #585c65;
20 | --tb-control-hover-color: #e1e1e1;
21 | --tb-control-close-symbol-color: #090909;
22 | }
23 | ::slotted(:not(.window__control)) {
24 | display: none;
25 | }
26 | @media (prefers-color-scheme: dark) {
27 | :host {
28 | background-color: var(--tb-theme-color, #1f1f1f);
29 | --tb-title-text-color: #cccccc;
30 | --tb-control-symbol-color: #cccccc;
31 | --tb-control-hover-color: #373737;
32 | --tb-control-close-symbol-color: #fcfcfc;
33 | }
34 | }
35 | .titlebar__drag-region {
36 | position: absolute;
37 | top: 0;
38 | left: 0;
39 | width: 100%;
40 | height: 100%;
41 | -webkit-app-region: drag;
42 | }
43 | .titlebar__title {
44 | display: flex;
45 | align-items: center;
46 | height: 100%;
47 | padding: 0 12px;
48 | font-family: var(--tb-title-font-family);
49 | font-size: 14px;
50 | font-weight: normal;
51 | color: var(--tb-title-text-color);
52 | }
53 | .titlebar__title.mac {
54 | justify-content: center;
55 | font-weight: 600;
56 | }
57 | .titlebar__window-controls {
58 | position: absolute;
59 | right: 0;
60 | top: 0;
61 | z-index: 10000;
62 | display: flex;
63 | -webkit-app-region: no-drag;
64 | }
65 | .titlebar__window-controls.mac {
66 | margin-top: var(--tb-control-margin);
67 | margin-right: var(--tb-control-margin);
68 | }
69 | .titlebar__window-controls.mac ::slotted(.window__control) {
70 | border-radius: var(--tb-control-radius);
71 | }
72 | .window__control, ::slotted(.window__control) {
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | width: var(--tb-control-width);
77 | min-width: 34px;
78 | height: var(--tb-control-height);
79 | min-height: 26px;
80 | }
81 | .window__control .window__control-icon {
82 | position: absolute;
83 | height: 10px;
84 | width: 10px;
85 | box-sizing: border-box;
86 | }
87 | .window__control:hover, ::slotted(.window__control:hover) {
88 | background-color: var(--tb-control-hover-color);
89 | }
90 | .window__control-min .window__control-icon:before {
91 | content: "";
92 | position: relative;
93 | top: 4px;
94 | display: flex;
95 | border-top: 1px solid var(--tb-control-symbol-color);
96 | }
97 | .window__control-max .window__control-icon {
98 | border: 1px solid var(--tb-control-symbol-color);
99 | }
100 | .window__control-max.window__control-max_active .window__control-icon {
101 | position: relative;
102 | border: none;
103 | }
104 | .window__control-max.window__control-max_active .window__control-icon:before,
105 | .window__control-max.window__control-max_active .window__control-icon:after {
106 | content: '';
107 | display: block;
108 | position: absolute;
109 | border: 1px solid var(--tb-control-symbol-color);
110 | }
111 | .window__control-max.window__control-max_active .window__control-icon:before {
112 | left: 0;
113 | bottom: 0;
114 | width: 6px;
115 | height: 6px;
116 | }
117 | .window__control-max.window__control-max_active .window__control-icon:after {
118 | top: 0;
119 | right: 0;
120 | width: 7px;
121 | height: 7px;
122 | border-left: 0;
123 | border-bottom: 0;
124 | background: linear-gradient(to left, var(--tb-control-symbol-color) 1px, transparent 0) no-repeat bottom right / 1px 1px, linear-gradient(to left, var(--tb-control-symbol-color) 1px, transparent 0) no-repeat top left / 1px 1px;
125 | }
126 | .window__control-close:hover {
127 | background-color: #F45454;
128 | }
129 | .window__control-close:hover .window__control-icon:before,
130 | .window__control-close:hover .window__control-icon:after {
131 | background-color: #fff;
132 | }
133 | .window__control-close .window__control-icon:before,
134 | .window__control-close .window__control-icon:after {
135 | content: '';
136 | position: absolute;
137 | top: 50%;
138 | left: -1px;
139 | width: 12px;
140 | height: 1px;
141 | background-color: var(--tb-control-close-symbol-color);
142 | }
143 | .window__control-close .window__control-icon:before {
144 | transform: rotate(45deg) translateZ(0);
145 | }
146 | .window__control-close .window__control-icon:after {
147 | transform: rotate(-45deg) translateZ(0);
148 | }
149 | `
150 |
151 | const OVERLAY_HOST_CSS = `
152 | :host {
153 | position: absolute;
154 | right: 0;
155 | top: 0;
156 | z-index: 10000;
157 | }
158 | `
159 |
160 | export default class TitleBar extends HTMLElement {
161 | constructor() {
162 | super()
163 | }
164 |
165 | static get observedAttributes(): string[] {
166 | return ['windowtitle']
167 | }
168 |
169 | attributeChangedCallback(
170 | name: string,
171 | oldValue: string,
172 | newValue: string
173 | ): void {
174 | if (name === 'windowtitle' && oldValue !== newValue) {
175 | this.updateTitle(newValue)
176 | }
177 | }
178 |
179 | connectedCallback(): void {
180 | const shadow = this.attachShadow({ mode: 'open' })
181 |
182 | const overlay = this.hasAttribute('overlay')
183 |
184 | const style = document.createElement('style')
185 | style.textContent = SHADOW_ROOT_CSS + (overlay ? OVERLAY_HOST_CSS : '')
186 | shadow.appendChild(style)
187 |
188 | const isMacintosh = core.process.platform === 'darwin'
189 |
190 | if (!overlay) {
191 | const el = document.createElement('div')
192 | el.classList.add('titlebar__drag-region')
193 | shadow.appendChild(el)
194 |
195 | const title = this.getAttribute('windowtitle')
196 | if (title) {
197 | this.updateTitle(title)
198 | }
199 | }
200 |
201 | if (!isMacintosh || this.hasChildNodes()) {
202 | const controls = document.createElement('div')
203 | controls.classList.add('titlebar__window-controls')
204 |
205 | if (isMacintosh) {
206 | controls.classList.add('mac')
207 | }
208 |
209 | controls.appendChild(document.createElement('slot'))
210 |
211 | if (!isMacintosh) {
212 | const minimizable = !this.hasAttribute('nominimize')
213 |
214 | if (minimizable) {
215 | const el = document.createElement('div')
216 | el.classList.add('window__control', 'window__control-min')
217 |
218 | const icon = document.createElement('span')
219 | icon.classList.add('window__control-icon')
220 |
221 | el.appendChild(icon)
222 |
223 | el.addEventListener('click', () => {
224 | core.ipcRenderer.send(TITLE_BAR_CHANNEL, 'minimize')
225 | })
226 |
227 | controls.appendChild(el)
228 | }
229 |
230 | const maximizable = !this.hasAttribute('nomaximize')
231 |
232 | if (maximizable) {
233 | const el = document.createElement('div')
234 | el.classList.add('window__control', 'window__control-max')
235 |
236 | const icon = document.createElement('span')
237 | icon.classList.add('window__control-icon')
238 |
239 | el.appendChild(icon)
240 |
241 | el.addEventListener('click', () => {
242 | core.ipcRenderer.send(TITLE_BAR_CHANNEL, 'maximizeOrUnmaximize')
243 | })
244 |
245 | core.ipcRenderer.on(
246 | TITLE_BAR_MAXIMIZE_REPLY_CHANNEL,
247 | (_, maximized: 0 | 1) => {
248 | if (maximized === 1) {
249 | el.classList.add('window__control-max_active')
250 | } else {
251 | el.classList.remove('window__control-max_active')
252 | }
253 | }
254 | )
255 |
256 | controls.appendChild(el)
257 | }
258 |
259 | const closable = !this.hasAttribute('noclose')
260 |
261 | if (closable) {
262 | const el = document.createElement('div')
263 | el.classList.add('window__control', 'window__control-close')
264 |
265 | const icon = document.createElement('span')
266 | icon.classList.add('window__control-icon')
267 |
268 | el.appendChild(icon)
269 |
270 | el.addEventListener('click', () => {
271 | core.ipcRenderer.send(TITLE_BAR_CHANNEL, 'close')
272 | })
273 |
274 | controls.appendChild(el)
275 | }
276 | }
277 |
278 | shadow.appendChild(controls)
279 | }
280 |
281 | const fullscreenable = this.hasAttribute('supportfullscreen')
282 |
283 | if (fullscreenable) {
284 | core.ipcRenderer.on(
285 | TITLE_BAR_FULLSCREEN_REPLY_CHANNEL,
286 | (_, fullscreen: 0 | 1) => {
287 | const root = this.shadowRoot
288 |
289 | if (root) {
290 | const region = root.querySelector(
291 | '.titlebar__drag-region'
292 | )
293 | if (region) {
294 | region.style['app-region'] = fullscreen === 1 ? 'no-drag' : ''
295 | }
296 |
297 | const controls = root.querySelector(
298 | '.titlebar__window-controls'
299 | )
300 |
301 | if (controls) {
302 | controls.style.display = fullscreen === 1 ? 'none' : ''
303 | }
304 | }
305 | }
306 | )
307 | }
308 | }
309 |
310 | updateTitle(title: string): void {
311 | const shadow = this.shadowRoot
312 | if (shadow) {
313 | let el = shadow.querySelector('.titlebar__title')
314 | if (el) {
315 | el.innerText = title
316 | } else {
317 | el = document.createElement('div')
318 | el.classList.add('titlebar__title')
319 | const isMacintosh = core.process.platform === 'darwin'
320 | if (isMacintosh) {
321 | el.classList.add('mac')
322 | }
323 | el.innerText = title
324 | shadow.appendChild(el)
325 | }
326 | }
327 | }
328 | }
329 |
330 | customElements.define('title-bar', TitleBar)
331 |
332 | export interface HTMLTitleBarElementAttributes {
333 | /**
334 | * Window title.
335 | */
336 | windowtitle?: string
337 | /**
338 | * Set window controls overlay.
339 | */
340 | overlay?: string
341 | /**
342 | * If specified, the title bar will not show the minimization control.
343 | */
344 | nominimize?: string
345 | /**
346 | * If specified, the title bar will not show the nomaximization control.
347 | */
348 | nomaximize?: string
349 | /**
350 | * If specified, the title bar will not show the `x` control.
351 | */
352 | noclose?: string
353 | /**
354 | * Support fullscreen window mode
355 | */
356 | supportfullscreen: string
357 | }
358 |
359 | /**
360 | * Title bar for Electron
361 | */
362 | export interface HTMLTitleBarElement
363 | extends HTMLElement,
364 | HTMLTitleBarElementAttributes {}
365 |
366 | declare global {
367 | interface HTMLElementTagNameMap {
368 | 'title-bar': HTMLTitleBarElement
369 | }
370 | }
371 |
--------------------------------------------------------------------------------
/packages/titlebar/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/toast/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### v1.0.0 (_2024-06-30_)
2 |
3 | -`@electron-uikit/toast` init
4 |
--------------------------------------------------------------------------------
/packages/toast/README.md:
--------------------------------------------------------------------------------
1 | # @electron-uikit/toast
2 |
3 | 
4 |
5 | Toast for Electron app.
6 |
7 | Toast is a concise, non-modal notification method that is used to briefly display information on the user interface without interrupting the user's current operation. It is widely used in mobile operating systems such as Android and iOS to provide quick feedback and important prompt information. Through toast notifications, developers can improve user experience and effectively communicate application status changes.
8 |
9 |
10 |
11 |
12 |
13 | ## Usage
14 |
15 | ### Install
16 |
17 | ```sh
18 | npm i @electron-uikit/core @electron-uikit/toast
19 | ```
20 |
21 | ### Get Started
22 |
23 | #### Using the toast in the renderer process.
24 |
25 | ```js
26 | import { toast } from '@electron-uikit/toast/renderer'
27 |
28 | toast.text('foo')
29 | toast.loading('loading')
30 | ```
31 |
32 | #### Using the toast in the main process.
33 |
34 | 1. Exposes the UI Kit APIs for components. See [@electron-uikit/core](https://github.com/alex8088/electron-uikit/tree/main/packages/core) guide for more details.
35 |
36 | You can expose it in the specified preload script:
37 |
38 | ```js
39 | import { exposeUIKit } from '@electron-uikit/core/preload'
40 |
41 | exposeUIKit()
42 | ```
43 |
44 | Or, you can expose it globally in the main process for all renderer processes:
45 |
46 | ```js
47 | import { useUIKit } from '@electron-uikit/core/main'
48 |
49 | useUIKit()
50 | ```
51 |
52 | > [!NOTE]
53 | > If you are using [@electron-toolkit/preload](https://github.com/alex8088/electron-toolkit/tree/master/packages/preload) to expose Electron APIs, there is no need to use this module, because `core` is also an export of it.
54 |
55 | 2. Register a listener in the renderer process, so that you can use it in the main process.
56 |
57 | ```js
58 | import { toast } from '@electron-uikit/toast/renderer'
59 |
60 | toast.config({
61 | supportMain: true
62 | })
63 | ```
64 |
65 | 3. Use the notification in the main process.
66 |
67 | ```js
68 | import { BrowserWindow } from 'electron'
69 | import { Toast } from '@electron-uikit/toast'
70 |
71 | const win = new BrowserWindow()
72 |
73 | const toast = new Toast(win)
74 | toast.text('foo')
75 | toast.loading('loading')
76 | ```
77 |
78 | ## APIs
79 |
80 | > [!NOTE]
81 | > To use Toast in the main process, you need to create a Toast instance of the specified window.
82 |
83 | ### `.config(options)`
84 |
85 | Configure toast defaults or customize toast. Can only be used in the renderer process.
86 |
87 | - options: `object`
88 |
89 | - **container**: `HTMLElement` (optional) - Container element of Toast. Default to `document.body`.
90 | - **duration**: `number` (optional) - Display duration in millisecond. If set to `0`, it will not turn off automatically. Default to `2000`.
91 | - **customClass**: `string` (optional) - Custom CSS class name for toast.
92 | - **bottom**: `number` (optional) - Toast position to the bottom. Default to `50`.
93 | - **maxWidth**: `number` (optional) - The maximum width of toast. Default to `320`.
94 | - **color**: `string` (optional) - Toast background color.
95 | - **textColor**: `string` (optional) - Toast text color.
96 | - **fontSize**: `number` (optional) - Toast font size. Default to `14`.
97 | - **iconSize**: `number` (optional) - Toast icon size. Default to `20`.
98 | - **supportMain**: `boolean` (optional) - Support Electron main process. Default to `false`.
99 |
100 | ### `.text(text[, duration])`
101 |
102 | Show text. The default duration is `2000` ms.
103 |
104 | ### `.loading(text[, duration])`
105 |
106 | Show loading. The default duration is 0, which means it is always displayed and can be turned off by calling its return value function.
107 |
108 | ```js
109 | import { toast } from '@electron-uikit/toast/renderer'
110 |
111 | const reply = toast.loading('Loading')
112 |
113 | setTimeout(() => {
114 | reply.success('Successful')
115 | // reply.fail('Failed')
116 | // reply.dismiss()
117 | }, 3000)
118 | ```
119 |
120 | ## Customization
121 |
122 | 1. Customize using CSS classes
123 |
124 | ```css
125 | .toast {
126 | --toast-bottom: 50px;
127 | --toast-z-index: 5001;
128 | --toast-color: #48484e;
129 | --toast-text-color: #ffffffd1;
130 | --toast-font-size: 14px;
131 | --toast-font-family: -apple-system, BlinkMacSystemFont, Ubuntu, 'Segoe UI';
132 | --toast-icon-size: 20px;
133 | --toast-max-width: 320px;
134 | }
135 | ```
136 |
137 | ```js
138 | toast.config({
139 | customClass: 'toast'
140 | })
141 | ```
142 |
143 | 2. Customize using `config` API
144 |
145 | ```js
146 | toast.config({
147 | bottom: 200,
148 | maxWidth: 280
149 | })
150 | ```
151 |
--------------------------------------------------------------------------------
/packages/toast/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@electron-uikit/toast",
3 | "version": "1.0.0",
4 | "description": "Toast for Electron app.",
5 | "main": "dist/main.cjs",
6 | "module": "dist/main.mjs",
7 | "types": "dist/main.d.ts",
8 | "exports": {
9 | ".": {
10 | "types": "./dist/main.d.ts",
11 | "import": "./dist/main.mjs",
12 | "require": "./dist/main.cjs"
13 | },
14 | "./main": {
15 | "types": "./dist/main.d.ts",
16 | "import": "./dist/main.mjs",
17 | "require": "./dist/main.cjs"
18 | },
19 | "./renderer": {
20 | "types": "./dist/renderer.d.ts",
21 | "import": "./dist/renderer.mjs"
22 | }
23 | },
24 | "typesVersions": {
25 | "*": {
26 | "main": [
27 | "./dist/main.d.ts"
28 | ],
29 | "renderer": [
30 | "./dist/renderer.d.ts"
31 | ]
32 | }
33 | },
34 | "files": [
35 | "dist"
36 | ],
37 | "author": "Alex Wei",
38 | "license": "MIT",
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/alex8088/electron-uikit.git",
42 | "directory": "packages/toast"
43 | },
44 | "bugs": {
45 | "url": "https://github.com/alex8088/electron-uikit/issues"
46 | },
47 | "homepage": "https://github.com/alex8088/electron-uikit/tree/master/packages/toast#readme",
48 | "keywords": [
49 | "electron",
50 | "toast"
51 | ],
52 | "scripts": {
53 | "build": "rollup -c rollup.config.ts --configPlugin typescript"
54 | },
55 | "peerDependencies": {
56 | "@electron-uikit/core": "*",
57 | "electron": ">=15.0.0"
58 | },
59 | "peerDependenciesMeta": {
60 | "@electron-uikit/core": {
61 | "optional": true
62 | }
63 | },
64 | "devDependencies": {
65 | "@electron-uikit/core": "workspace:^"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/toast/rollup.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */
2 | import { defineConfig } from 'rollup'
3 | import resolve from '@rollup/plugin-node-resolve'
4 | import commonjs from '@rollup/plugin-commonjs'
5 | import ts from '@rollup/plugin-typescript'
6 | import dts from 'rollup-plugin-dts'
7 | import rm from 'rollup-plugin-rm'
8 |
9 | export default defineConfig([
10 | {
11 | input: ['src/main.ts'],
12 | output: [
13 | {
14 | entryFileNames: '[name].cjs',
15 | chunkFileNames: 'chunks/lib-[hash].cjs',
16 | format: 'cjs',
17 | dir: 'dist'
18 | },
19 | {
20 | entryFileNames: '[name].mjs',
21 | chunkFileNames: 'chunks/lib-[hash].mjs',
22 | format: 'es',
23 | dir: 'dist'
24 | }
25 | ],
26 | external: ['electron'],
27 | plugins: [
28 | resolve(),
29 | commonjs(),
30 | ts({
31 | compilerOptions: {
32 | rootDir: 'src',
33 | declaration: true,
34 | outDir: 'dist/types'
35 | }
36 | }),
37 | rm('dist', 'buildStart')
38 | ]
39 | },
40 | {
41 | input: ['src/renderer.ts'],
42 | output: [
43 | { file: './dist/renderer.mjs', format: 'es' },
44 | { name: 'renderer', file: './dist/renderer.js', format: 'iife' }
45 | ],
46 | plugins: [
47 | ts({
48 | compilerOptions: {
49 | rootDir: 'src',
50 | declaration: true,
51 | outDir: 'dist/types'
52 | }
53 | })
54 | ]
55 | },
56 | {
57 | input: ['dist/types/main.d.ts'],
58 | output: [{ file: './dist/main.d.ts', format: 'es' }],
59 | plugins: [dts()],
60 | external: ['electron']
61 | },
62 | {
63 | input: ['dist/types/renderer.d.ts'],
64 | output: [{ file: './dist/renderer.d.ts', format: 'es' }],
65 | plugins: [dts(), rm('dist/types', 'buildEnd')]
66 | }
67 | ])
68 |
--------------------------------------------------------------------------------
/packages/toast/screenshots/toast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex8088/electron-uikit/37a35a1ead8d8018e1581ebbf7c7336f199ec1b2/packages/toast/screenshots/toast.png
--------------------------------------------------------------------------------
/packages/toast/src/common.ts:
--------------------------------------------------------------------------------
1 | import type { UIKitAPI } from '@electron-uikit/core'
2 |
3 | export const core = ((globalThis || window).uikit ||
4 | (globalThis || window).electron) as UIKitAPI
5 |
--------------------------------------------------------------------------------
/packages/toast/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const TOAST_CHANNEL = 'uikit:toast'
2 |
--------------------------------------------------------------------------------
/packages/toast/src/main.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow } from 'electron'
2 | import { TOAST_CHANNEL } from './constants'
3 |
4 | import type { ToastType, ToastLoadingFn } from './types'
5 |
6 | export type { ToastLoadingFn } from './types'
7 |
8 | /**
9 | * Toast for Electron main process. Before use, you must ensure that
10 | * `toast.config({ supportMain: true }` is configured in the renderer.
11 | *
12 | * @example
13 | *
14 | * ```
15 | * import { BrowserWindow } from 'electron'
16 | * import { Toast } from '@electron-uikit/toast'
17 | *
18 | * const win = new BrowserWindow()
19 | *
20 | * const toast = new Toast(win)
21 | * toast.text('foo')
22 | * toast.loading('loading')
23 | * ```
24 | */
25 | export class Toast {
26 | constructor(readonly win: BrowserWindow) {}
27 |
28 | private show(text: string, type: ToastType, duration?: number): void {
29 | this.win.webContents.send(TOAST_CHANNEL, 1, text, type, duration)
30 | }
31 |
32 | private close(): void {
33 | this.win.webContents.send(TOAST_CHANNEL, 0)
34 | }
35 |
36 | /**
37 | * Show text. The default duration is `2000` ms.
38 | */
39 | text(text: string, duration?: number): void {
40 | this.show(text, 'text', duration)
41 | }
42 |
43 | /**
44 | * Show loading. The default duration is 0, which means it is always displayed
45 | * and can be turned off by calling its return value function.
46 | *
47 | * @example
48 | *
49 | * ```
50 | * import { Toast } from '@electron-uikit/toast'
51 | *
52 | * const toast = new Toast(win)
53 | * const reply = toast.loading('Loading')
54 | *
55 | * setTimeout(() => {
56 | * reply.success('Successful')
57 | * }, 3000)
58 | * ```
59 | */
60 | loading(text: string, duration = 0): ToastLoadingFn {
61 | this.show(text, 'loading', duration)
62 | return {
63 | success: (text, duration) => this.show(text, 'success', duration),
64 | fail: (text, duration) => this.show(text, 'failed', duration),
65 | dismiss: () => this.close()
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/packages/toast/src/renderer.ts:
--------------------------------------------------------------------------------
1 | import { core } from './common'
2 | import { TOAST_CHANNEL } from './constants'
3 |
4 | import type { ToastType, ToastLoadingFn } from './types'
5 |
6 | export type { ToastLoadingFn } from './types'
7 |
8 | const SHADOW_ROOT_CSS = `
9 | :host {
10 | position: fixed;
11 | left: 0;
12 | right: 0;
13 | bottom: var(--toast-bottom, 50px);
14 | z-index: var(--toast-z-index, 5001);
15 | display: flex;
16 | flex-direction: column;
17 | align-items: center;
18 | justify-content: flex-end;
19 | height: 0;
20 | overflow: visible;
21 | user-select: none;
22 | --toast-color: #48484e;
23 | --toast-text-color: #ffffffd1;
24 | --toast-font-size: 14px;
25 | --toast-font-family: -apple-system, BlinkMacSystemFont, Ubuntu, 'Segoe UI';
26 | --toast-icon-size: 20px;
27 | --toast-icon-margin: 0 8px 0 0;
28 | --toast-padding: 6px 14px;
29 | --toast-border-radius: 4px;
30 | --toast-max-width: 320px;
31 | --toast-box-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05);
32 | }
33 | @keyframes fadeIn {
34 | from {
35 | opacity: 0;
36 | }
37 | to {
38 | opacity: 1;
39 | }
40 | }
41 | @keyframes fadeOut {
42 | from {
43 | opacity: 1;
44 | }
45 | to {
46 | opacity: 0;
47 | }
48 | }
49 | @keyframes loading {
50 | 0% {
51 | transform: rotate3d(0, 0, 1, 0deg);
52 | }
53 |
54 | 100% {
55 | transform: rotate3d(0, 0, 1, 360deg);
56 | }
57 | }
58 | .fade-in {
59 | animation: fadeIn ease 0.3s forwards;
60 | }
61 | .fade-out {
62 | animation: fadeOut ease 0.3s forwards;
63 | }
64 | .icon-loading {
65 | animation: loading 1s infinite cubic-bezier(0, 0, 1, 1);
66 | mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 48 48' fill='none' xmlns='http://www.w3.org/2000/svg' stroke='currentColor' stroke-width='4'%3E%3Cpath d='M42 24c0 9.941-8.059 18-18 18S6 33.941 6 24 14.059 6 24 6'%3E%3C/path%3E%3C/svg%3E");
67 | }
68 | .icon-success {
69 | mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M116 569a28 28 0 0 1-3-37l21-26c9-11 25-14 36-6l180 120c9 6 26 6 35-2l476-386c10-8 27-8 37 2l11 11c11 11 10 27-1 37L397 793a41 41 0 0 1-58-2L116 569z'/%3E%3C/svg%3E");
70 | }
71 | .icon-failed {
72 | mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M223 854a55 55 0 0 1-39-94l572-572a55 55 0 1 1 78 78L263 838a55 55 0 0 1-40 16z'/%3E%3Cpath d='M795 854a55 55 0 0 1-39-16L184 266a55 55 0 1 1 79-78l571 572a55 55 0 0 1-39 94z'/%3E%3C/svg%3E");
73 | }
74 | .toast {
75 | box-sizing: border-box;
76 | display: flex;
77 | align-items: center;
78 | padding: var(--toast-padding);
79 | border-radius: var(--toast-border-radius);
80 | flex-wrap: nowrap;
81 | overflow: hidden;
82 | max-width: var(--toast-max-width);
83 | color: var(--toast-text-color);
84 | background-color: var(--toast-color);
85 | box-shadow: var(--toast-box-shadow);
86 | }
87 | .toast__icon {
88 | margin: var(--toast-icon-margin);
89 | height: var(--toast-icon-size);
90 | width: var(--toast-icon-size);
91 | font-size: var(--toast-icon-size);
92 | mask-position: 50% 50%;
93 | mask-repeat: no-repeat;
94 | mask-size: 100%;
95 | background-color: currentColor;
96 | }
97 | .toast__content {
98 | display: inline-block;
99 | line-height: 1.4;
100 | font-size: var(--toast-font-size);
101 | font-family: var(--toast-font-family);
102 | word-break: break-all;
103 | text-align: center;
104 | }
105 | `
106 |
107 | export type ToastOptions = {
108 | /**
109 | * Container element of Toast. Default to `document.body`
110 | */
111 | container?: HTMLElement
112 | /**
113 | * Display duration in millisecond. If set to `0`, it will not turn off
114 | * automatically. Default to `2000`.
115 | */
116 | duration?: number
117 | /**
118 | * Custom CSS class name for toast.
119 | */
120 | customClass?: string
121 | /**
122 | * Toast position to the bottom. Default to `50`.
123 | */
124 | bottom?: number
125 | /**
126 | * The maximum width of toast. Default to `320`.
127 | */
128 | maxWidth?: number
129 | /**
130 | * Toast background color.
131 | */
132 | color?: string
133 | /**
134 | * Toast text color.
135 | */
136 | textColor?: string
137 | /**
138 | * Toast font size. Default to `14`.
139 | */
140 | fontSize?: number
141 | /**
142 | * Toast icon size. Default to `20`.
143 | */
144 | iconSize?: number
145 | /**
146 | * Support Electron main process. Default to `false`.
147 | */
148 | supportMain?: boolean
149 | }
150 |
151 | class Toast {
152 | private shadowHost: HTMLElement | null = null
153 | private shadowRoot: ShadowRoot | null = null
154 | private view: HTMLElement | null = null
155 | private timeout: unknown | null = null
156 |
157 | private container?: HTMLElement
158 | private duration = 2000
159 | private customClass?: string
160 | private customStyle: Record = {}
161 |
162 | private h(text: string, type: ToastType = 'text'): HTMLElement {
163 | const view = document.createElement('div')
164 |
165 | const toast = document.createElement('div')
166 | toast.classList.add('toast')
167 |
168 | if (type !== 'text') {
169 | const icon = document.createElement('i')
170 | icon.classList.add('toast__icon')
171 | icon.classList.add(`icon-${type}`)
172 | toast.appendChild(icon)
173 | }
174 |
175 | const content = document.createElement('div')
176 | content.classList.add('toast__content')
177 | content.innerText = text
178 |
179 | toast.appendChild(content)
180 |
181 | view.appendChild(toast)
182 |
183 | return view
184 | }
185 |
186 | private show(text: string, type: ToastType, duration: number): void {
187 | if (this.shadowHost) {
188 | if (this.timeout) {
189 | clearTimeout(this.timeout as number)
190 | }
191 | this.shadowRoot!.removeChild(this.view!)
192 |
193 | this.view = this.h(text, type)
194 |
195 | this.shadowRoot!.appendChild(this.view)
196 | } else {
197 | this.shadowHost = document.createElement('div')
198 | if (this.customClass) {
199 | this.shadowHost.classList.add(this.customClass)
200 | }
201 |
202 | if (this.customStyle) {
203 | for (const [key, value] of Object.entries(this.customStyle)) {
204 | this.shadowHost.style.setProperty(key, value)
205 | }
206 | }
207 |
208 | this.shadowRoot = this.shadowHost.attachShadow({ mode: 'open' })
209 |
210 | const style = document.createElement('style')
211 | style.textContent = SHADOW_ROOT_CSS
212 | this.shadowRoot.appendChild(style)
213 |
214 | this.view = this.h(text, type)
215 |
216 | this.shadowRoot.appendChild(this.view)
217 |
218 | this.view.classList.add('fade-in')
219 |
220 | if (this.container) {
221 | this.container.appendChild(this.shadowHost)
222 | } else {
223 | document.body.appendChild(this.shadowHost)
224 | }
225 | }
226 |
227 | if (duration > 0) {
228 | this.timeout = setTimeout(() => {
229 | this.view!.classList.add('fade-out')
230 | this.view!.addEventListener('animationend', () => {
231 | this.shadowRoot!.removeChild(this.view!)
232 | this.shadowRoot = null
233 | this.shadowHost!.remove()
234 | this.shadowHost = null
235 | this.view = null
236 | this.timeout = null
237 | })
238 | }, duration)
239 | }
240 | }
241 |
242 | private close(): void {
243 | if (this.shadowHost) {
244 | if (this.timeout) {
245 | clearTimeout(this.timeout as number)
246 | }
247 | this.view!.classList.add('fade-out')
248 | this.view!.addEventListener('animationend', () => {
249 | this.shadowRoot!.removeChild(this.view!)
250 | this.shadowRoot = null
251 | this.shadowHost!.remove()
252 | this.shadowHost = null
253 | this.view = null
254 | this.timeout = null
255 | })
256 | }
257 | }
258 |
259 | /**
260 | * Configure toast defaults or customize toast.
261 | */
262 | config(options: ToastOptions): void {
263 | const {
264 | container,
265 | customClass,
266 | duration,
267 | bottom,
268 | maxWidth,
269 | color,
270 | textColor,
271 | fontSize,
272 | iconSize
273 | } = options
274 |
275 | this.container = container
276 | this.duration = duration || this.duration
277 | this.customClass = customClass
278 |
279 | if (bottom) {
280 | this.customStyle['--toast-bottom'] = `${bottom}px`
281 | }
282 | if (maxWidth) {
283 | this.customStyle['--toast-max-width'] = `${maxWidth}px`
284 | }
285 | if (color) {
286 | this.customStyle['--toast-color'] = color
287 | }
288 | if (textColor) {
289 | this.customStyle['--toast-text-color'] = textColor
290 | }
291 | if (fontSize) {
292 | this.customStyle['--toast-font-size'] = `${fontSize}px`
293 | }
294 | if (iconSize) {
295 | this.customStyle['--toast-icon-size'] = `${iconSize}px`
296 | }
297 |
298 | if (options.supportMain) {
299 | core.ipcRenderer.on(
300 | TOAST_CHANNEL,
301 | (
302 | _,
303 | action: 0 | 1,
304 | text: string,
305 | type: ToastType,
306 | duration?: number
307 | ) => {
308 | if (action === 1) {
309 | this.show(text, type, duration ?? this.duration)
310 | } else {
311 | this.close()
312 | }
313 | }
314 | )
315 | }
316 | }
317 |
318 | /**
319 | * Show text. The default duration is `2000` ms.
320 | */
321 | text(text: string, duration = this.duration): void {
322 | this.show(text, 'text', duration)
323 | }
324 |
325 | /**
326 | * Show loading. The default duration is 0, which means it is always displayed
327 | * and can be turned off by calling its return value function.
328 | *
329 | * @example
330 | *
331 | * ```
332 | * import { toast } from '@electron-uikit/toast/renderer'
333 | *
334 | * const reply = toast.loading('Loading')
335 | *
336 | * setTimeout(() => {
337 | * reply.success('Successful')
338 | * }, 3000)
339 | * ```
340 | */
341 | loading(text: string, duration = 0): ToastLoadingFn {
342 | this.show(text, 'loading', duration)
343 | return {
344 | success: (text, duration = this.duration) =>
345 | this.show(text, 'success', duration),
346 | fail: (text, duration = this.duration) =>
347 | this.show(text, 'failed', duration),
348 | dismiss: () => this.close()
349 | }
350 | }
351 | }
352 |
353 | export const toast = new Toast()
354 |
--------------------------------------------------------------------------------
/packages/toast/src/types.ts:
--------------------------------------------------------------------------------
1 | export type ToastType = 'text' | 'loading' | 'success' | 'failed'
2 |
3 | export type ToastLoadingFn = {
4 | /**
5 | * Toggle loading toast to success status.
6 | */
7 | success: (text: string, duration?: number) => void
8 | /**
9 | * Toggle loading toast to failed state.
10 | */
11 | fail: (text: string, duration?: number) => void
12 | /**
13 | * Dismiss loading toast.
14 | */
15 | dismiss: () => void
16 | }
17 |
--------------------------------------------------------------------------------
/packages/toast/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "lib": ["es2022", "esnext", "dom"],
6 | "sourceMap": false,
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "moduleResolution": "Bundler",
10 | "resolveJsonModule": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "skipLibCheck": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "noImplicitAny": false,
16 | "noImplicitReturns": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "include": ["packages"],
4 | "exclude": ["**/dist"]
5 | }
6 |
--------------------------------------------------------------------------------