Files
knock-gui/desktop-angular/ELECTRON_NATIVE_MODALS.md
2025-10-03 15:05:48 +06:00

155 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Electron-native Modals Guide
This guide documents the Electron-native modal dialogs implemented in this app. These modals are opened from the Electron main process (via a small HTML dialog window) and are configurable at runtime (text, buttons, and colors).
## Why Electron-native modals?
- Work even if the Angular UI is busy/frozen
- True application-level modal (owned by main process)
- One-shot dialogs you can call from anywhere in renderer through IPC
- Per-dialog runtime theming (background/text/button colors)
## File Map
- `src/main/main.js`
- IPC handler `dialog:custom`
- Helper `openCustomModalWindow(config)` that creates a modal `BrowserWindow`
- `src/main/modal.html`
- Self-contained dialog UI (HTML/CSS/JS)
- Listens for `custom-modal:config` and renders content/buttons
- Sends result back via `custom-modal:result`
- `src/preload/preload.js`
- Exposes `showNativeModal(config)` to `window.api`
- `src/frontend/src/app/ipc.service.ts`
- Adds `showNativeModal(config)` convenience wrapper for Angular code
## Runtime API
Call from Angular (renderer):
```ts
const result = await this.ipc.showNativeModal({
title: 'Confirm Deletion',
message: 'Are you sure you want to delete the item?',
buttons: [
{ id: 'cancel', label: 'Cancel', style: 'secondary' },
{ id: 'delete', label: 'Delete', style: 'danger' }
],
colors: {
background: '#aa1c3a',
text: '#ffffff',
buttonBg: '#ffffff',
buttonText: '#aa1c3a',
secondaryBg: 'rgba(255,255,255,0.1)',
secondaryText: '#ffffff'
},
buttonStyles: {
delete: { bg: '#e53935', text: '#fff' },
cancel: { bg: 'rgba(255,255,255,0.1)', text: '#ffffff' }
}
});
// result => { buttonId: string, buttonIndex: number, buttonLabel?: string }
```
### Config Schema
```ts
interface NativeModalButton {
id: string; // identifier returned as buttonId
label: string; // text on the button
style?: 'primary' | 'secondary' | 'danger'; // default visual style
}
interface NativeModalConfig {
title?: string; // dialog title
message?: string; // main message (plain text)
buttons?: NativeModalButton[]; // 1..3 buttons (extra are ignored)
colors?: { // global color overrides
background?: string; // dialog background
text?: string; // title/message text color
buttonBg?: string; // primary button bg
buttonText?: string; // primary button text color
secondaryBg?: string; // secondary button bg
secondaryText?: string; // secondary button text color
};
buttonStyles?: { // per-button overrides by button id
[buttonId: string]: {
bg?: string; // background + border color
text?: string; // text color
}
};
}
```
## Color Model
The dialog uses CSS variables in `modal.html` with sensible defaults matching the app footer theme:
- `--bg` (default `#aa1c3a`)
- `--text` (default `#ffffff`)
- `--btn-bg` / `--btn-text` (primary buttons)
- `--btn-sec-bg` / `--btn-sec-text` (secondary buttons)
You can override them per-dialog via `colors` and refine specific buttons through `buttonStyles`.
## Button Styles
- `primary`: white background with red text (matches app footer theme)
- `secondary`: translucent white background with white text
- `danger`: red background (`#e53935`) with white text
You can still override any of these via `buttonStyles` per button.
## Result Contract
The renderer receives:
```ts
{ buttonId: string, buttonIndex: number, buttonLabel?: string }
```
If the dialog window is closed without a click, youll get `{ buttonId: 'closed', buttonIndex: -1 }`.
## How it Works (Flow)
1. Renderer calls `window.api.showNativeModal(config)` (exposed by preload)
2. Main process handles `dialog:custom`, opens a modal `BrowserWindow` and loads `modal.html`
3. After the page loads, main sends `custom-modal:config` with the payload
4. The page renders content and buttons; on click it emits `custom-modal:result`
5. Main resolves the original IPC with `{ buttonId, buttonIndex, buttonLabel }`
## Example: Yes/No/Cancel With Custom Colors
```ts
await this.ipc.showNativeModal({
title: 'Save Changes',
message: 'Save before closing?',
buttons: [
{ id: 'yes', label: 'Yes', style: 'primary' },
{ id: 'no', label: 'No', style: 'secondary' },
{ id: 'cancel', label: 'Cancel', style: 'danger' }
],
colors: {
background: '#1e293b',
text: '#e2e8f0',
buttonBg: '#e2e8f0',
buttonText: '#1e293b',
secondaryBg: 'rgba(226,232,240,0.12)',
secondaryText: '#e2e8f0'
}
});
```
## Notes & Considerations
- Maximum 3 buttons are rendered (extra are ignored)
- Message is plain text (no HTML injection)
- The dialog is frameless and always-on-top, sized 560x340 by default
- Parent is the currently focused window, when available
- Closing the dialog without a click returns `{ buttonId: 'closed' }`
## Rationale
- Keep Angular modal for in-app UX consistency and speed
- Add Electron-native modal for cases where UI thread may be busy, or when deep theming and app-level modality are desired