updated breakcore theme

This commit is contained in:
2026-05-15 15:44:08 +02:00
parent 85b699739b
commit 7294dd47ef
8 changed files with 501 additions and 9 deletions
+131
View File
@@ -0,0 +1,131 @@
/*
* Imperative confirm() / notify() that replace native window.confirm /
* window.alert. The site hydrates many independent React islands that share
* no React root, so this is a vanilla DOM singleton: any island (or inline
* script) can `await confirmDialog(...)`. Styling is class-driven, so it
* inherits every theme — including the breakcore neon/hazard layer — for free.
*/
export interface ConfirmOptions {
title: string;
message?: string;
confirmLabel?: string;
cancelLabel?: string;
/** Visual weight of the confirm button. Defaults to 'danger'. */
tone?: 'danger' | 'primary';
}
let activeCleanup: (() => void) | null = null;
export function confirmDialog(opts: ConfirmOptions): Promise<boolean> {
// Collapse any in-flight dialog (treat as cancel) before opening a new one.
activeCleanup?.();
return new Promise<boolean>(resolve => {
const {
title,
message,
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
tone = 'danger',
} = opts;
const lastFocused = document.activeElement as HTMLElement | null;
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const overlay = document.createElement('div');
overlay.className = 'cdialog-overlay';
overlay.setAttribute('role', 'alertdialog');
overlay.setAttribute('aria-modal', 'true');
const backdrop = document.createElement('div');
backdrop.className = 'cdialog-backdrop';
backdrop.setAttribute('aria-hidden', 'true');
const panel = document.createElement('div');
panel.className = 'glass cdialog-panel';
const titleId = `cdlg-t-${Math.random().toString(36).slice(2)}`;
const h = document.createElement('h2');
h.className = 'cdialog-title';
h.id = titleId;
h.textContent = title;
overlay.setAttribute('aria-labelledby', titleId);
panel.appendChild(h);
if (message) {
const p = document.createElement('p');
p.className = 'cdialog-msg';
p.textContent = message;
panel.appendChild(p);
}
const actions = document.createElement('div');
actions.className = 'cdialog-actions';
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'btn btn--ghost';
cancelBtn.textContent = cancelLabel;
const confirmBtn = document.createElement('button');
confirmBtn.type = 'button';
confirmBtn.className = `btn ${tone === 'primary' ? 'btn--primary' : 'btn--danger'}`;
confirmBtn.textContent = confirmLabel;
actions.append(cancelBtn, confirmBtn);
panel.appendChild(actions);
overlay.append(backdrop, panel);
document.body.appendChild(overlay);
let settled = false;
function close(result: boolean) {
if (settled) return;
settled = true;
activeCleanup = null;
document.removeEventListener('keydown', onKey, true);
document.body.style.overflow = prevOverflow;
overlay.remove();
lastFocused?.focus?.();
resolve(result);
}
function onKey(e: KeyboardEvent) {
if (e.key === 'Escape') {
e.preventDefault();
close(false);
} else if (e.key === 'Tab') {
// Two-stop focus trap.
e.preventDefault();
const next = document.activeElement === confirmBtn ? cancelBtn : confirmBtn;
next.focus();
} else if (e.key === 'Enter' && document.activeElement === confirmBtn) {
e.preventDefault();
close(true);
}
}
backdrop.addEventListener('click', () => close(false));
cancelBtn.addEventListener('click', () => close(false));
confirmBtn.addEventListener('click', () => close(true));
document.addEventListener('keydown', onKey, true);
activeCleanup = () => close(false);
// Destructive default: focus Cancel so an accidental Enter is safe.
(tone === 'primary' ? confirmBtn : cancelBtn).focus();
});
}
/** Transient bottom-center toast. Replaces window.alert for failures. */
export function notify(message: string, tone: 'error' | 'success' = 'error') {
document.querySelector('.toast[data-notify]')?.remove();
const el = document.createElement('div');
el.className = `toast${tone === 'error' ? ' toast--error' : ''}`;
el.dataset.notify = '';
el.setAttribute('role', tone === 'error' ? 'alert' : 'status');
el.textContent = message;
el.addEventListener('click', () => el.remove());
document.body.appendChild(el);
window.setTimeout(() => el.remove(), 4500);
}