updated breakcore theme
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user