@manufosela/app-modal

@manufosela/app-modal

<dialog> on steroids — modal web component with focus trap, configurable buttons, theming, and two operation modes. Built on native <dialog> for accessibility.

App Modal

<dialog> on steroids — modal with focus trap, configurable buttons, theming, and two operation modes.

Declarative Usage (Recommended)

Use the open attribute to control visibility. Modal persists in DOM and can be reused.

Waiting for action...
<app-modal id="myModal" title="Confirm Delete" message="Are you sure you want to delete this item?" button1-text="Delete" button2-text="Cancel" button1-css="background: #ef4444;" ></app-modal> <script> const modal = document.getElementById('myModal'); // Show modal modal.open = true; // Listen for button events modal.addEventListener('modal-action1', () => { console.log('Deleted!'); }); modal.addEventListener('modal-action2', () => { console.log('Cancelled'); }); </script>

Declarative with Slotted Content

Use slots for custom content instead of the message attribute.

<app-modal id="slottedModal" title="Custom Content" button1-text="Got it"> <p>This content is <strong>slotted</strong> into the modal.</p> <ul> <li>Custom HTML</li> <li>Full flexibility</li> </ul> </app-modal>

Programmatic Usage

Basic Modal

import { showModal } from '@manufosela/app-modal'; showModal({ title: 'Hello!', message: 'Basic modal dialog.', button1Text: 'OK' });

Confirmation Dialog

Waiting for action...
showModal({ title: 'Confirm Action', message: 'Delete this item?', button1Text: 'Delete', button2Text: 'Cancel', button1Css: 'background: #ef4444;', button1Action: () => console.log('Deleted!'), button2Action: () => console.log('Cancelled.') });

Three Button Modal

Waiting for action...
showModal({ title: 'Save Changes?', message: 'You have unsaved changes.', button1Text: 'Save', button2Text: 'Discard', button3Text: 'Cancel', button1Css: 'background: #22c55e;', button2Css: 'background: #ef4444;', button1Action: () => console.log('Saved!'), button2Action: () => console.log('Discarded.'), button3Action: () => console.log('Cancelled.') });

Custom Sizes

// Small modal showModal({ title: 'Small', message: 'Max-width 300px.', maxWidth: '300px', button1Text: 'OK' }); // Large modal showModal({ title: 'Large', message: 'Max-width 600px.', maxWidth: '600px', button1Text: 'OK' });

Without Header/Footer

// No header showModal({ message: 'No header.', showHeader: false, button1Text: 'Got it' }); // No footer showModal({ title: 'No Footer', message: 'Close with X or Escape.', showFooter: false });

Intercept Close

Use intercept-close to prevent auto-close and handle the close request externally.

Waiting for action...
<app-modal id="interceptModal" intercept-close modal-id="intercept-demo" title="Unsaved Changes" message="You have unsaved changes. Are you sure you want to close?" button1-text="Save & Close" button2-text="Discard" ></app-modal> <script> const modal = document.getElementById('interceptModal'); // Listen for close request (X button, Escape, overlay click) document.addEventListener('modal-closed-requested', (e) => { if (e.detail.modalId === 'intercept-demo') { // Ask for confirmation before closing if (confirm('Close without saving?')) { document.dispatchEvent(new CustomEvent('close-modal', { detail: { modalId: 'intercept-demo' } })); } } }); </script>

HTML Message

showModal({ title: 'Rich Content', message: ` <h3 style="margin:0">Features</h3> <ul> <li><strong>Bold</strong></li> <li><em>Italic</em></li> </ul> `, button1Text: 'Nice!' });

This content is slotted into the modal.

  • Custom HTML
  • Full flexibility
Demo code (CodePen-ready HTML, CSS, JS)
HTML (html)
<h1>App Modal</h1>
<p class="subtitle">&lt;dialog&gt; on steroids — modal with focus trap, configurable buttons, theming, and two operation modes.</p>
<div class="demo-links">
  <a href="https://manufosela.dev/ui-components/">← Back to components</a>
  <a href="https://github.com/manufosela/ui-components/tree/main/packages/app-modal" target="_blank" rel="noopener">GitHub Repo</a>
  <a href="playground.html">Playground</a>
  <a href="https://www.npmjs.com/package/@manufosela/app-modal" target="_blank" rel="noopener">npm</a>
</div>
<div class="demo-theme-toggle">
  <theme-toggle theme="dark"></theme-toggle>
</div>
<div class="demo-section">
    <h2>Declarative Usage (Recommended)</h2>
    <p style="color: #1d1d1f;">Use the <code>open</code> attribute to control visibility. Modal persists in DOM and can be reused.</p>
    <div class="button-grid">
      <button id="showDeclarative">Show Declarative Modal</button>
    </div>
    <div class="output" id="declarativeOutput">Waiting for action...</div>
    <div class="code-block">&lt;app-modal
  id="myModal"
  title="Confirm Delete"
  message="Are you sure you want to delete this item?"
  button1-text="Delete"
  button2-text="Cancel"
  button1-css="background: #ef4444;"
&gt;&lt;/app-modal&gt;

&lt;script&gt;
  const modal = document.getElementById('myModal');

  // Show modal
  modal.open = true;

  // Listen for button events
  modal.addEventListener('modal-action1', () => {
    console.log('Deleted!');
  });
  modal.addEventListener('modal-action2', () => {
    console.log('Cancelled');
  });
&lt;/script&gt;</div>
      
  </div>

  <div class="demo-section">
    <h2>Declarative with Slotted Content</h2>
    <p style="color: #1d1d1f;">Use slots for custom content instead of the message attribute.</p>
    <div class="button-grid">
      <button id="showSlotted">Show Slotted Modal</button>
    </div>
    <div class="code-block">&lt;app-modal id="slottedModal" title="Custom Content" button1-text="Got it"&gt;
  &lt;p&gt;This content is &lt;strong&gt;slotted&lt;/strong&gt; into the modal.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;Custom HTML&lt;/li&gt;
    &lt;li&gt;Full flexibility&lt;/li&gt;
  &lt;/ul&gt;
&lt;/app-modal&gt;</div>
  </div>

  <h2 style="margin-top: 2rem; color: #86868b;">Programmatic Usage</h2>

  <div class="demo-section">
    <h2>Basic Modal</h2>
    <div class="button-grid">
      <button id="showBasic">Show Basic Modal</button>
    </div>
    <div class="code-block">import { showModal } from '@manufosela/app-modal';

showModal({
  title: 'Hello!',
  message: 'Basic modal dialog.',
  button1Text: 'OK'
});</div>
  </div>

  <div class="demo-section">
    <h2>Confirmation Dialog</h2>
    <div class="button-grid">
      <button id="showConfirm">Show Confirmation</button>
    </div>
    <div class="output" id="confirmOutput">Waiting for action...</div>
    <div class="code-block">showModal({
  title: 'Confirm Action',
  message: 'Delete this item?',
  button1Text: 'Delete',
  button2Text: 'Cancel',
  button1Css: 'background: #ef4444;',
  button1Action: () => console.log('Deleted!'),
  button2Action: () => console.log('Cancelled.')
});</div>
  </div>

  <div class="demo-section">
    <h2>Three Button Modal</h2>
    <div class="button-grid">
      <button id="showThree">Show 3-Button Modal</button>
    </div>
    <div class="output" id="threeOutput">Waiting for action...</div>
    <div class="code-block">showModal({
  title: 'Save Changes?',
  message: 'You have unsaved changes.',
  button1Text: 'Save',
  button2Text: 'Discard',
  button3Text: 'Cancel',
  button1Css: 'background: #22c55e;',
  button2Css: 'background: #ef4444;',
  button1Action: () => console.log('Saved!'),
  button2Action: () => console.log('Discarded.'),
  button3Action: () => console.log('Cancelled.')
});</div>
  </div>

  <div class="demo-section">
    <h2>Custom Sizes</h2>
    <div class="button-grid">
      <button id="showSmall">Small (300px)</button>
      <button id="showLarge">Large (600px)</button>
    </div>
    <div class="code-block">// Small modal
showModal({
  title: 'Small',
  message: 'Max-width 300px.',
  maxWidth: '300px',
  button1Text: 'OK'
});

// Large modal
showModal({
  title: 'Large',
  message: 'Max-width 600px.',
  maxWidth: '600px',
  button1Text: 'OK'
});</div>
  </div>

  <div class="demo-section">
    <h2>Without Header/Footer</h2>
    <div class="button-grid">
      <button id="showNoHeader">No Header</button>
      <button id="showNoFooter">No Footer</button>
    </div>
    <div class="code-block">// No header
showModal({
  message: 'No header.',
  showHeader: false,
  button1Text: 'Got it'
});

// No footer
showModal({
  title: 'No Footer',
  message: 'Close with X or Escape.',
  showFooter: false
});</div>
  </div>

  <div class="demo-section">
    <h2>Intercept Close</h2>
    <p style="color: #1d1d1f;">Use <code>intercept-close</code> to prevent auto-close and handle the close request externally.</p>
    <div class="button-grid">
      <button id="showIntercept">Show Intercepted Modal</button>
    </div>
    <div class="output" id="interceptOutput">Waiting for action...</div>
    <div class="code-block">&lt;app-modal
  id="interceptModal"
  intercept-close
  modal-id="intercept-demo"
  title="Unsaved Changes"
  message="You have unsaved changes. Are you sure you want to close?"
  button1-text="Save &amp; Close"
  button2-text="Discard"
&gt;&lt;/app-modal&gt;

&lt;script&gt;
  const modal = document.getElementById('interceptModal');

  // Listen for close request (X button, Escape, overlay click)
  document.addEventListener('modal-closed-requested', (e) => {
    if (e.detail.modalId === 'intercept-demo') {
      // Ask for confirmation before closing
      if (confirm('Close without saving?')) {
        document.dispatchEvent(new CustomEvent('close-modal', {
          detail: { modalId: 'intercept-demo' }
        }));
      }
    }
  });
&lt;/script&gt;</div>
  </div>

  <div class="demo-section">
    <h2>HTML Message</h2>
    <div class="button-grid">
      <button id="showHtml">Show HTML Modal</button>
    </div>
    <div class="code-block">showModal({
  title: 'Rich Content',
  message: `
    &lt;h3 style="margin:0"&gt;Features&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Bold&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;&lt;em&gt;Italic&lt;/em&gt;&lt;/li&gt;
    &lt;/ul&gt;
  `,
  button1Text: 'Nice!'
});</div>
  </div>

  <!-- Declarative modals (hidden by default, controlled by open property) -->
  <app-modal
    id="declarativeModal"
    title="Confirm Delete"
    message="Are you sure you want to delete this item?"
    button1-text="Delete"
    button2-text="Cancel"
    button1-css="background: #ef4444;"
  ></app-modal>

  <app-modal
    id="slottedModal"
    title="Custom Content"
    button1-text="Got it"
  >
    <p>This content is <strong>slotted</strong> into the modal.</p>
    <ul>
      <li>Custom HTML</li>
      <li>Full flexibility</li>
    </ul>
  </app-modal>

  <app-modal
    id="interceptModal"
    intercept-close
    modal-id="intercept-demo"
    title="Unsaved Changes"
    message="You have unsaved changes. Are you sure you want to close?"
    button1-text="Save & Close"
    button2-text="Discard"
  ></app-modal>
CSS (css)
:root {
  --bg: #0c0f14;
  --bg-elevated: #141923;
  --bg-panel: #171d28;
  --border: #262f3f;
  --text: #f4f6fb;
  --text-muted: #a7b0c2;
  --text-dim: #7d879b;
  --accent: #ff8a3d;
  --accent-strong: #ff6a00;
  --accent-soft: rgba(255, 138, 61, 0.16);
  --shadow: 0 20px 50px rgba(5, 8, 14, 0.45);
  --radius-lg: 22px;
  --radius-md: 14px;
  --radius-sm: 10px;
  --max-width: 1160px;
}

h1 { color: #1d1d1f; }
    .demo-section {
      background: white;
      padding: 2rem;
      border-radius: 12px;
      margin-bottom: 2rem;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    h2 { margin-top: 0; color: #1d1d1f; }
    .button-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
      gap: 1rem;
      margin-top: 1rem;
    }
    button {
      padding: 1rem;
      border: none;
      border-radius: 8px;
      background: #3b82f6;
      color: white;
      cursor: pointer;
      font-size: 1rem;
      font-weight: 500;
      transition: transform 0.2s;
    }
    button:hover { transform: translateY(-2px); }
    a { color: #3b82f6; }
    p { color: #86868b; }
    .output {
      margin-top: 1rem;
      padding: 1rem;
      background: #f5f5f7;
      border-radius: 8px;
      font-family: monospace;
    }
    .code-block {
      background: #1f2937;
      color: #e5e7eb;
      padding: 1rem;
      border-radius: 8px;
      margin-top: 1rem;
      overflow-x: auto;
      font-family: 'SF Mono', Monaco, monospace;
      font-size: 0.85rem;
      white-space: pre;
    }
  
    footer {
      text-align: center;
      margin-top: 3rem;
      padding-top: 2rem;
      border-top: 1px solid #d2d2d7;
      color: #86868b;
      font-size: 0.9rem;
    }
    footer a { color: #1d1d1f; text-decoration: none; }
    footer a:hover { text-decoration: underline; }
  


@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&display=swap');

:root {
  --bg: #f5f5f7;
  --bg-2: #ffffff;
  --bg-spot-1: #f8e9d0;
  --bg-spot-2: #e8eef8;
  --card: #ffffff;
  --text: #1d1d1f;
  --muted: #6b7280;
  --line: #e5e7eb;
  --accent: #ffb000;
  --accent-2: #00a7d6;
  --accent-soft: rgba(255, 176, 0, 0.18);
  --surface-1: #f3f4f6;
  --surface-2: #eef2f7;
  --code-bg: #111827;
  --code-text: #f9fafb;
  --panel-bg: rgba(255, 255, 255, 0.85);
  --overlay-bg: rgba(255, 255, 255, 0.98);
}

:root.dark {
  --bg: #0f1117;
  --bg-2: #151a26;
  --bg-spot-1: #1a2136;
  --bg-spot-2: #1d1b34;
  --card: #1c2233;
  --text: #f3f6ff;
  --muted: #b8c0d9;
  --line: #2b3247;
  --accent: #ffb000;
  --accent-2: #00d0ff;
  --accent-soft: rgba(255, 176, 0, 0.25);
  --surface-1: #0b0f1a;
  --surface-2: #263046;
  --code-bg: #0b0f1a;
  --code-text: #d6d9e6;
  --panel-bg: rgba(28, 34, 51, 0.8);
  --overlay-bg: rgba(15, 17, 23, 0.98);
}

* {
  box-sizing: border-box;
}

a {
  color: var(--accent);
}

h1 {
  color: var(--accent);
}

.demo-links {
  margin-top: 12px;
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
}

.demo-links a {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 6px 12px;
  color: var(--muted);
  text-decoration: none;
  font-size: 0.85rem;
  transition: border-color 0.2s ease, color 0.2s ease;
}

.demo-links a:hover {
  color: var(--text);
  border-color: var(--accent-2);
}

.demo-theme-toggle {
  margin-top: 12px;
  display: flex;
  justify-content: center;
}

header {
  border-bottom: 1px solid var(--line);
}

.demo-card,
.section,
.demo-section,
.panel,
.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 16px;
  color: var(--text);
  box-shadow: 0 18px 36px rgba(6, 10, 24, 0.45);
}

.demo-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.demo-card .code-block {
  margin-top: auto;
}

.demo-grid,
.grid {
  align-items: stretch;
}

.demo-card h2,
.demo-card h3,
.section h2,
.demo-section h2,
.panel-header {
  color: var(--text);
}

.label,
.stat-label,
.category-title,
.subtitle,
.hint,
.note {
  color: var(--muted);
}

.info-item,
.capability,
.preference,
.option-group,
.output,
.current-url,
.event-log,
.result-card,
.log,
.stat {
  background: var(--surface-1);
  border: 1px solid var(--line);
  color: var(--text);
}

.info-item .label,
.capability .name {
  color: var(--muted);
}

.panel-header,
.options,
.topbar,
.top-links {
  background: var(--panel-bg);
  border-bottom: 1px solid var(--line);
  color: var(--text);
}

.subtitle,
.hint,
.note,
.demo-card p,
.section p,
.demo-section p {
  color: var(--muted);
}

.code-block,
pre,
code {
  background: var(--code-bg);
  color: var(--code-text);
  border-radius: 8px;
}

.value-display,
.output,
.result,
.demo-output {
  background: var(--surface-1);
  border: 1px solid var(--line);
  color: var(--text);
}

button {
  background: linear-gradient(120deg, var(--accent), #ff6a00);
  color: #111;
  border: none;
}

button:hover {
  filter: brightness(1.05);
}

input,
select,
textarea {
  background: var(--surface-1);
  color: var(--text);
  border: 1px solid var(--line);
}

footer {
  color: var(--muted);
}

footer a {
  color: var(--text);
}

arc-slider {
  --arc-slider-text-color: var(--text);
  --arc-slider-value-bg: var(--surface-1);
  --arc-slider-value-border: var(--line);
  --arc-slider-value-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
}

rich-select {
  --caller-background: var(--card);
  --caller-color: var(--text);
  --caller-border: 1px solid var(--line);
  --caller-hover-background: var(--surface-2);
  --caller-hover-color: var(--text);
  --caller-hover-border-color: var(--line);
  --caller-focus-border-color: var(--accent-2);
  --caller-focus-shadow: 0 0 0 3px rgba(0, 208, 255, 0.25);
  --caller-disabled-background: var(--surface-1);
  --caller-disabled-color: #6b7280;
  --caller-disabled-border-color: var(--line);
  --arrow-color: var(--muted);
  --selectOptions-background: var(--card);
  --selectOptions-border: 1px solid var(--line);
  --selectOptions-shadow: 0 10px 22px rgba(0, 0, 0, 0.45);
  --input-background: var(--surface-1);
  --input-border: 1px solid var(--line);
  --input-color: var(--text);
  --input-placeholder-color: #6b7280;
  --option-color: var(--text);
  --option-hover-background: var(--surface-2);
  --option-hover-color: var(--text);
  --option-active-background: var(--accent);
  --option-active-color: #111;
  --option-selected-background: var(--accent-soft);
  --option-selected-color: var(--text);
  --option-disabled-background: var(--surface-1);
  --option-disabled-color: #6b7280;
}

multi-select {
  --multi-select-bg: var(--card);
  --multi-select-border-color: var(--line);
  --multi-select-border-hover: var(--line);
  --multi-select-border-focus: var(--accent-2);
  --multi-select-text-color: var(--text);
  --multi-select-placeholder-color: #6b7280;
  --multi-select-arrow-color: var(--muted);
  --multi-select-dropdown-bg: var(--card);
  --multi-select-shadow: 0 10px 22px rgba(0, 0, 0, 0.45);
  --multi-select-option-hover-bg: var(--surface-2);
  --multi-select-option-selected-bg: var(--accent-soft);
}

tab-nav {
  --tab-bg: var(--card);
  --tab-border: var(--line);
  --tab-text: var(--muted);
  --tab-active-text: var(--text);
  --tab-hover-bg: var(--surface-2);
  --tab-active-border: var(--accent);
  --tab-disabled: #9ca3af;
}

slider-underline {
  --slider-track: var(--surface-2);
  --slider-fill: var(--accent);
  --slider-thumb: var(--accent);
  --slider-label-color: var(--text);
  --slider-tick-color: #9ca3af;
  --slider-tick-value-color: var(--muted);
}

header-nav {
  --header-bg: var(--card);
  --header-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
  --header-link-color: var(--text);
  --header-link-hover: var(--accent);
  --header-mobile-hover-bg: var(--surface-2);
}

calendar-inline {
  --calendar-bg: var(--card);
  --calendar-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
  --calendar-text: var(--text);
  --calendar-accent: var(--accent);
  --calendar-today: var(--accent-soft);
  --calendar-selected: var(--accent);
  --calendar-hover-bg: var(--surface-2);
  --calendar-muted: var(--muted);
  --calendar-muted-strong: #9ca3af;
  --calendar-other-month: #9ca3af;
  --calendar-disabled: #cbd5e1;
  --calendar-holiday: #ef4444;
  --calendar-holiday-selected: #111;
}

marked-calendar {
  --calendar-bg: var(--card);
  --calendar-border: var(--line);
  --calendar-title: var(--text);
  --calendar-muted: var(--muted);
  --calendar-surface: var(--surface-1);
  --calendar-accent: var(--accent);
  --calendar-accent-hover: #ff7a1a;
  --calendar-border-strong: var(--line);
  --calendar-contrast: #111;
  --calendar-nav-bg: var(--surface-1);
  --calendar-nav-hover: var(--surface-2);
}

radar-chart {
  --radar-bg: var(--card);
  --radar-grid-color: var(--line);
  --radar-axis-color: #94a3b8;
  --radar-label-color: var(--muted);
}

multi-carousel {
  --carousel-bg: var(--card);
  --carousel-arrow-bg: var(--surface-1);
  --carousel-arrow-color: var(--text);
  --carousel-arrow-hover-bg: var(--surface-2);
  --carousel-arrow-hover-color: var(--text);
  --carousel-nav-bg: var(--surface-1);
  --carousel-nav-color: var(--muted);
  --carousel-nav-hover: #9ca3af;
  --carousel-nav-active: var(--accent);
  --carousel-focus-color: var(--accent-2);
}

nav-list {
  --nav-list-bg: var(--card);
  --nav-list-border-color: var(--line);
  --nav-list-selected-border-color: var(--accent);
  --nav-list-selected-bg: var(--surface-2);
  --nav-list-hover-bg: var(--surface-2);
  --nav-list-selected-color: var(--text);
}

theme-toggle {
  --theme-toggle-bg: var(--card);
  --theme-toggle-icon-color: var(--muted);
  --theme-toggle-hover-bg: var(--surface-2);
  --theme-toggle-active-bg: var(--surface-1);
  --theme-toggle-active-color: var(--text);
  --theme-toggle-dark-bg: var(--card);
  --theme-toggle-dark-border: var(--line);
  --theme-toggle-dark-icon-color: var(--muted);
  --theme-toggle-dark-active-bg: var(--surface-1);
  --theme-toggle-dark-active-color: var(--text);
  --theme-toggle-dark-hover-bg: var(--surface-2);
}

qr-code {
  --qr-fg: #0f1117;
  --qr-bg: #f3f6ff;
}

click-clock {
  --clock-color: var(--text);
  --clock-bg: var(--card);
  --clock-muted-color: var(--muted);
}

historical-line {
  --title-color: var(--text);
  --border-color: var(--line);
  --year-bg: var(--surface-1);
}

circle-steps {
  --steps-muted: var(--muted);
  --steps-text: var(--text);
  --steps-pending: var(--surface-2);
}

rich-inputfile {
  --input-border: var(--line);
  --input-border-focus: var(--accent-2);
  --input-bg: var(--card);
  --input-label-color: var(--text);
  --input-hover-bg: var(--surface-2);
  --input-drag-bg: var(--accent-soft);
  --input-disabled-bg: var(--surface-1);
  --input-success-border: #22c55e;
  --input-success-bg: rgba(34, 197, 94, 0.12);
  --input-icon-color: #94a3b8;
  --input-text-color: var(--muted);
  --input-accent-color: var(--accent-2);
  --input-file-bg: var(--surface-1);
  --input-preview-bg: var(--surface-1);
  --input-file-name-color: var(--text);
  --input-file-size-color: var(--muted);
  --input-error-color: #ef4444;
  --input-hint-color: var(--muted);
}

data-card {
  --data-card-bg: var(--card);
  --data-card-border-color: var(--line);
  --data-card-title-color: var(--text);
  --data-card-desc-color: var(--muted);
  --data-card-info-bg: var(--overlay-bg);
  --data-card-info-close-bg: var(--surface-2);
  --data-card-info-close-color: var(--text);
  --data-card-info-close-hover-bg: var(--surface-1);
  --data-card-info-text: var(--text);
  --data-card-loading-color: var(--muted);
  --data-card-info-trigger-hover: var(--accent);
}

app-modal {
  --app-modal-bg: var(--card);
  --app-modal-body-color: var(--text);
  --app-modal-standalone-bg: rgba(255, 176, 0, 0.35);
  --app-modal-standalone-color: #111;
  --app-modal-standalone-hover-bg: rgba(255, 176, 0, 0.6);
}
JS (js)
document.querySelectorAll('.footer-year').forEach(el => el.textContent = new Date().getFullYear());
  

    import "https://esm.sh/@manufosela/app-modal";
    import { showModal } from "https://esm.sh/@manufosela/app-modal";

    // Get declarative modals (hidden by default, no initialization needed)
    const declarativeModal = document.getElementById('declarativeModal');
    const slottedModal = document.getElementById('slottedModal');
    const interceptModal = document.getElementById('interceptModal');

    // Declarative modal event handlers
    document.getElementById('showDeclarative').addEventListener('click', () => {
      declarativeModal.open = true;
    });

    declarativeModal.addEventListener('modal-action1', () => {
      document.getElementById('declarativeOutput').textContent = 'Deleted!';
    });
    declarativeModal.addEventListener('modal-action2', () => {
      document.getElementById('declarativeOutput').textContent = 'Cancelled.';
    });

    // Slotted modal
    document.getElementById('showSlotted').addEventListener('click', () => {
      slottedModal.open = true;
    });

    // Intercept close modal
    document.getElementById('showIntercept').addEventListener('click', () => {
      interceptModal.open = true;
      document.getElementById('interceptOutput').textContent = 'Modal opened. Try clicking X, pressing Escape, or clicking outside...';
    });

    interceptModal.addEventListener('modal-action1', () => {
      document.getElementById('interceptOutput').textContent = 'Saved and closed!';
    });
    interceptModal.addEventListener('modal-action2', () => {
      document.getElementById('interceptOutput').textContent = 'Discarded changes.';
    });

    // Listen for close request on intercept modal
    document.addEventListener('modal-closed-requested', (e) => {
      if (e.detail.modalId === 'intercept-demo') {
        document.getElementById('interceptOutput').textContent = 'Close requested - showing confirmation...';
        // Ask for confirmation before closing
        if (confirm('You have unsaved changes. Close anyway?')) {
          document.getElementById('interceptOutput').textContent = 'Closed without saving.';
          document.dispatchEvent(new CustomEvent('close-modal', {
            detail: { modalId: 'intercept-demo' }
          }));
        } else {
          document.getElementById('interceptOutput').textContent = 'Close cancelled - modal stays open.';
        }
      }
    });

    // Programmatic examples
    document.getElementById('showBasic').addEventListener('click', () => {
      showModal({ title: 'Hello!', message: 'Basic modal dialog.', button1Text: 'OK' });
    });

    document.getElementById('showConfirm').addEventListener('click', () => {
      showModal({
        title: 'Confirm Action',
        message: 'Delete this item?',
        button1Text: 'Delete',
        button2Text: 'Cancel',
        button1Css: 'background: #ef4444;',
        button1Action: () => { document.getElementById('confirmOutput').textContent = 'Deleted!'; },
        button2Action: () => { document.getElementById('confirmOutput').textContent = 'Cancelled.'; }
      });
    });

    document.getElementById('showThree').addEventListener('click', () => {
      showModal({
        title: 'Save Changes?',
        message: 'You have unsaved changes.',
        button1Text: 'Save',
        button2Text: 'Discard',
        button3Text: 'Cancel',
        button1Css: 'background: #22c55e;',
        button2Css: 'background: #ef4444;',
        button1Action: () => { document.getElementById('threeOutput').textContent = 'Saved!'; },
        button2Action: () => { document.getElementById('threeOutput').textContent = 'Discarded.'; },
        button3Action: () => { document.getElementById('threeOutput').textContent = 'Cancelled.'; }
      });
    });

    document.getElementById('showSmall').addEventListener('click', () => {
      showModal({ title: 'Small', message: 'Max-width 300px.', maxWidth: '300px', button1Text: 'OK' });
    });

    document.getElementById('showLarge').addEventListener('click', () => {
      showModal({ title: 'Large', message: 'Max-width 600px.', maxWidth: '600px', button1Text: 'OK' });
    });

    document.getElementById('showNoHeader').addEventListener('click', () => {
      showModal({ message: 'No header.', showHeader: false, button1Text: 'Got it' });
    });

    document.getElementById('showNoFooter').addEventListener('click', () => {
      showModal({ title: 'No Footer', message: 'Close with X or Escape.', showFooter: false });
    });

    document.getElementById('showHtml').addEventListener('click', () => {
      showModal({
        title: 'Rich Content',
        message: '<h3 style="margin:0">Features</h3><ul><li><strong>Bold</strong></li><li><em>Italic</em></li></ul>',
        button1Text: 'Nice!'
      });
    });
  

  import '../../theme-toggle/src/theme-toggle.js';

  const root = document.documentElement;

  const toggle = document.querySelector('theme-toggle');
  if (toggle) {
    toggle.theme = root.classList.contains('dark') ? 'dark' : 'light';
    toggle.addEventListener('theme-changed', (event) => {
      const theme = event.detail?.theme;
      if (!theme) return;
      root.classList.toggle('dark', theme === 'dark');
    });
  }