Files
knock-gui/desktop-angular/ELECTRON_NATIVE_MODALS.md

5.0 KiB
Raw Permalink Blame History

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):

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 text
  • danger: 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, 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

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