likwid/frontend/src/components/ui/FeedbackHost.astro

265 lines
7.8 KiB
Text
Raw Normal View History

<div id="likwid-toasts" class="ui-toasts" aria-live="polite" aria-atomic="true"></div>
<dialog id="likwid-dialog" class="ui-dialog">
<form method="dialog" class="ui-dialog-form">
<div class="ui-dialog-head">
<h3 id="likwid-dialog-title" class="ui-dialog-title"></h3>
<p id="likwid-dialog-message" class="ui-dialog-message"></p>
</div>
<div id="likwid-dialog-input-wrap" class="ui-dialog-input" style="display: none;">
<label class="ui-label" for="likwid-dialog-textarea" id="likwid-dialog-label"></label>
<textarea id="likwid-dialog-textarea" class="ui-textarea" rows="4"></textarea>
</div>
<div class="ui-dialog-actions">
<button id="likwid-dialog-cancel" class="ui-btn ui-btn-secondary" value="cancel" type="submit">Cancel</button>
<button id="likwid-dialog-confirm" class="ui-btn ui-btn-primary" value="confirm" type="submit">Confirm</button>
</div>
</form>
</dialog>
<script is:inline>
(function() {
const toastsEl = document.getElementById('likwid-toasts');
const dialogEl = document.getElementById('likwid-dialog');
const dialogTitleEl = document.getElementById('likwid-dialog-title');
const dialogMessageEl = document.getElementById('likwid-dialog-message');
const dialogInputWrapEl = document.getElementById('likwid-dialog-input-wrap');
const dialogLabelEl = document.getElementById('likwid-dialog-label');
const dialogTextareaEl = document.getElementById('likwid-dialog-textarea');
const dialogConfirmEl = document.getElementById('likwid-dialog-confirm');
const dialogCancelEl = document.getElementById('likwid-dialog-cancel');
function toast(type, message, opts) {
if (!toastsEl) return;
const options = opts || {};
const id = 't_' + Math.random().toString(16).slice(2);
const el = document.createElement('div');
el.className = 'ui-toast ui-toast-' + (type || 'info');
el.setAttribute('role', type === 'error' ? 'alert' : 'status');
el.dataset.toastId = id;
const text = document.createElement('div');
text.className = 'ui-toast-text';
text.textContent = String(message || '');
const close = document.createElement('button');
close.type = 'button';
close.className = 'ui-toast-close';
close.setAttribute('aria-label', 'Dismiss notification');
close.textContent = '×';
close.addEventListener('click', () => {
el.remove();
});
el.appendChild(text);
el.appendChild(close);
toastsEl.appendChild(el);
const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : 4500;
if (timeoutMs > 0) {
window.setTimeout(() => {
el.remove();
}, timeoutMs);
}
}
function openDialog(mode, opts) {
const options = opts || {};
if (!dialogEl || !dialogTitleEl || !dialogMessageEl || !dialogConfirmEl || !dialogCancelEl) {
return Promise.resolve(mode === 'prompt' ? null : false);
}
if (typeof dialogEl.showModal !== 'function') {
if (mode === 'prompt') {
return Promise.resolve(window.prompt(options.message || options.title || '', ''));
}
return Promise.resolve(window.confirm(options.message || options.title || 'Confirm?'));
}
dialogTitleEl.textContent = String(options.title || '');
dialogMessageEl.textContent = String(options.message || '');
const isPrompt = mode === 'prompt';
if (dialogInputWrapEl && dialogTextareaEl && dialogLabelEl) {
dialogInputWrapEl.style.display = isPrompt ? 'block' : 'none';
dialogLabelEl.textContent = String(options.label || '');
dialogTextareaEl.value = String(options.defaultValue || '');
dialogTextareaEl.placeholder = String(options.placeholder || '');
}
dialogConfirmEl.textContent = String(options.confirmText || (isPrompt ? 'Submit' : 'Confirm'));
dialogCancelEl.textContent = String(options.cancelText || 'Cancel');
return new Promise((resolve) => {
function cleanup() {
dialogEl.removeEventListener('close', onClose);
}
function onClose() {
const rv = dialogEl.returnValue;
if (mode === 'prompt') {
if (rv !== 'confirm') {
cleanup();
resolve(null);
return;
}
const v = dialogTextareaEl ? dialogTextareaEl.value : '';
cleanup();
resolve(v);
return;
}
cleanup();
resolve(rv === 'confirm');
}
dialogEl.addEventListener('close', onClose);
dialogEl.showModal();
if (mode === 'prompt' && dialogTextareaEl) {
dialogTextareaEl.focus();
} else {
dialogConfirmEl.focus();
}
});
}
window.likwidUi = window.likwidUi || {};
window.likwidUi.toast = toast;
window.likwidUi.confirm = function(opts) { return openDialog('confirm', opts); };
window.likwidUi.prompt = function(opts) { return openDialog('prompt', opts); };
})();
</script>
<style>
.ui-toasts {
position: fixed;
top: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
z-index: 9999;
width: min(420px, calc(100vw - 2rem));
pointer-events: none;
}
.ui-toast {
pointer-events: auto;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
padding: 0.875rem 1rem;
border-radius: 12px;
border: 1px solid var(--color-border);
background: color-mix(in srgb, var(--color-surface) 92%, transparent);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.22);
}
.ui-toast-text {
color: var(--color-text);
font-size: 0.9375rem;
line-height: 1.35;
white-space: pre-wrap;
word-break: break-word;
}
.ui-toast-close {
appearance: none;
background: transparent;
border: 0;
color: var(--color-text-muted);
font-size: 1.25rem;
line-height: 1;
cursor: pointer;
padding: 0;
margin: -2px 0 0 0;
}
.ui-toast-close:hover {
color: var(--color-text);
}
.ui-toast-success {
border-color: color-mix(in srgb, var(--color-success) 45%, var(--color-border));
background: color-mix(in srgb, var(--color-success-muted) 65%, var(--color-surface));
}
.ui-toast-error {
border-color: color-mix(in srgb, var(--color-error) 45%, var(--color-border));
background: color-mix(in srgb, var(--color-error-muted) 65%, var(--color-surface));
}
.ui-toast-info {
border-color: color-mix(in srgb, var(--color-info) 45%, var(--color-border));
background: color-mix(in srgb, var(--color-info-muted) 65%, var(--color-surface));
}
.ui-dialog {
border: 1px solid var(--color-border);
border-radius: 14px;
padding: 0;
background: var(--color-surface);
color: var(--color-text);
width: min(560px, calc(100vw - 2rem));
}
.ui-dialog::backdrop {
background: var(--color-overlay);
}
.ui-dialog-form {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.ui-dialog-title {
margin: 0;
font-size: 1.05rem;
}
.ui-dialog-message {
margin: 0.25rem 0 0 0;
color: var(--color-text-muted);
font-size: 0.9375rem;
line-height: 1.4;
}
.ui-dialog-input {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.ui-label {
font-weight: 600;
font-size: 0.9375rem;
}
.ui-textarea {
width: 100%;
border-radius: 12px;
border: 1px solid var(--color-border);
background: var(--color-field-bg);
color: var(--color-text);
padding: 0.75rem;
font-size: 0.9375rem;
resize: vertical;
min-height: 88px;
}
.ui-dialog-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
</style>