5.0 KiB
5.0 KiB
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 modalBrowserWindow
- IPC handler
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)
towindow.api
- Exposes
src/frontend/src/app/ipc.service.ts
- Adds
showNativeModal(config)
convenience wrapper for Angular code
- Adds
Runtime API
Call from Angular (renderer):
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
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 textdanger
: red background (#e53935
) with white text
You can still override any of these via buttonStyles
per button.
Result Contract
The renderer receives:
{ buttonId: string, buttonIndex: number, buttonLabel?: string }
If the dialog window is closed without a click, you’ll get { buttonId: 'closed', buttonIndex: -1 }
.
How it Works (Flow)
- Renderer calls
window.api.showNativeModal(config)
(exposed by preload) - Main process handles
dialog:custom
, opens a modalBrowserWindow
and loadsmodal.html
- After the page loads, main sends
custom-modal:config
with the payload - The page renders content and buttons; on click it emits
custom-modal:result
- Main resolves the original IPC with
{ buttonId, buttonIndex, buttonLabel }
Example: Yes/No/Cancel With Custom Colors
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