diff --git a/assets/app.js b/assets/app.js
index 36ff758..57d577d 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -38,6 +38,9 @@ import './styles/03-components/search.css';
import './styles/03-components/image-upload.css';
import './styles/03-components/zaps.css';
+// Toast notifications
+import './styles/toast.css';
+
// Editor layout
import './styles/editor-layout.css';
diff --git a/assets/controllers/nostr/nostr_publish_controller.js b/assets/controllers/nostr/nostr_publish_controller.js
index 1ff58b9..1e505df 100644
--- a/assets/controllers/nostr/nostr_publish_controller.js
+++ b/assets/controllers/nostr/nostr_publish_controller.js
@@ -509,38 +509,45 @@ export default class extends Controller {
}
showStatus(message) {
- if (this.hasStatusTarget) {
+ // Use toast system if available, otherwise fallback to status target
+ if (typeof window.showToast === 'function') {
+ window.showToast(message, 'info', 3000);
+ } else if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `
${message}
`;
+ // Clear status after 3 seconds
+ setTimeout(() => {
+ if (this.hasStatusTarget) {
+ this.statusTarget.innerHTML = '';
+ }
+ }, 3000);
}
- // Clear status after 3 seconds
- setTimeout(() => {
- if (this.hasStatusTarget) {
- this.statusTarget.innerHTML = '';
- }
- }, 3000);
}
showSuccess(message) {
- if (this.hasStatusTarget) {
+ if (typeof window.showToast === 'function') {
+ window.showToast(message, 'success', 3000);
+ } else if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `${message}
`;
+ // Clear status after 3 seconds
+ setTimeout(() => {
+ if (this.hasStatusTarget) {
+ this.statusTarget.innerHTML = '';
+ }
+ }, 3000);
}
- // Clear status after 3 seconds
- setTimeout(() => {
- if (this.hasStatusTarget) {
- this.statusTarget.innerHTML = '';
- }
- }, 3000);
}
showError(message) {
- if (this.hasStatusTarget) {
+ if (typeof window.showToast === 'function') {
+ window.showToast(message, 'danger', 10000);
+ } else if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `${message}
`;
+ // Clear status after 10 seconds
+ setTimeout(() => {
+ if (this.hasStatusTarget) {
+ this.statusTarget.innerHTML = '';
+ }
+ }, 10000);
}
- // Clear status after 10 seconds
- setTimeout(() => {
- if (this.hasStatusTarget) {
- this.statusTarget.innerHTML = '';
- }
- }, 10000);
}
}
diff --git a/assets/controllers/nostr/nostr_single_sign_controller.js b/assets/controllers/nostr/nostr_single_sign_controller.js
index 00a1b11..760afe5 100644
--- a/assets/controllers/nostr/nostr_single_sign_controller.js
+++ b/assets/controllers/nostr/nostr_single_sign_controller.js
@@ -189,17 +189,24 @@ export default class extends Controller {
}
showStatus(message) {
- if (this.hasStatusTarget) {
+ // Use toast system if available, otherwise fallback to status target
+ if (typeof window.showToast === 'function') {
+ window.showToast(message, 'info');
+ } else if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `${message}
`;
}
}
showSuccess(message) {
- if (this.hasStatusTarget) {
+ if (typeof window.showToast === 'function') {
+ window.showToast(message, 'success');
+ } else if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `${message}
`;
}
}
showError(message) {
- if (this.hasStatusTarget) {
+ if (typeof window.showToast === 'function') {
+ window.showToast(message, 'danger');
+ } else if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `${message}
`;
}
}
diff --git a/assets/controllers/utility/toast_controller.js b/assets/controllers/utility/toast_controller.js
new file mode 100644
index 0000000..8f58e04
--- /dev/null
+++ b/assets/controllers/utility/toast_controller.js
@@ -0,0 +1,162 @@
+import { Controller } from '@hotwired/stimulus';
+
+/**
+ * Central toast notification controller
+ *
+ * Usage from other controllers:
+ *
+ * // Get the toast controller instance
+ * const toastController = this.application.getControllerForElementAndIdentifier(
+ * document.querySelector('[data-controller~="utility--toast"]'),
+ * 'utility--toast'
+ * );
+ *
+ * // Show a toast
+ * toastController.show('Success!', 'success');
+ * toastController.show('Error occurred', 'danger');
+ * toastController.show('Processing...', 'info');
+ * toastController.show('Warning!', 'warning');
+ *
+ * // Or use the global helper (recommended)
+ * window.showToast('Success!', 'success');
+ */
+export default class extends Controller {
+ static targets = ['container'];
+
+ connect() {
+ console.log('Toast controller connected');
+ this.queue = [];
+ this.currentToast = null;
+ this.isProcessing = false;
+
+ // Expose globally for easy access from any controller
+ window.showToast = (message, type = 'info', duration = 4000) => {
+ this.show(message, type, duration);
+ };
+ }
+
+ disconnect() {
+ // Clean up global reference
+ if (window.showToast) {
+ delete window.showToast;
+ }
+ }
+
+ /**
+ * Show a toast notification
+ * @param {string} message - The message to display
+ * @param {string} type - Type of toast: 'success', 'danger', 'warning', 'info'
+ * @param {number} duration - How long to show the toast in milliseconds (default: 4000)
+ */
+ show(message, type = 'info', duration = 4000) {
+ this.queue.push({ message, type, duration });
+ this.processQueue();
+ }
+
+ /**
+ * Process the toast queue one at a time
+ */
+ processQueue() {
+ // If already showing a toast, wait
+ if (this.isProcessing) {
+ return;
+ }
+
+ // If queue is empty, nothing to do
+ if (this.queue.length === 0) {
+ return;
+ }
+
+ this.isProcessing = true;
+ const { message, type, duration } = this.queue.shift();
+
+ // Create toast element
+ const toast = this.createToastElement(message, type);
+ this.containerTarget.appendChild(toast);
+ this.currentToast = toast;
+
+ // Trigger animation after a small delay (for CSS transition)
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ toast.classList.add('toast--show');
+ });
+ });
+
+ // Auto-dismiss after duration
+ setTimeout(() => {
+ this.dismissToast(toast);
+ }, duration);
+ }
+
+ /**
+ * Create a toast DOM element
+ * @param {string} message
+ * @param {string} type
+ * @returns {HTMLElement}
+ */
+ createToastElement(message, type) {
+ const toast = document.createElement('div');
+ toast.className = `toast toast--${type}`;
+ toast.setAttribute('role', 'alert');
+ toast.setAttribute('aria-live', 'polite');
+ toast.setAttribute('aria-atomic', 'true');
+
+ const content = document.createElement('div');
+ content.className = 'toast__content';
+ content.textContent = message;
+
+ const closeButton = document.createElement('button');
+ closeButton.className = 'toast__close';
+ closeButton.setAttribute('type', 'button');
+ closeButton.setAttribute('aria-label', 'Close');
+ closeButton.innerHTML = '×';
+ closeButton.addEventListener('click', () => this.dismissToast(toast));
+
+ toast.appendChild(content);
+ toast.appendChild(closeButton);
+
+ return toast;
+ }
+
+ /**
+ * Dismiss a toast with animation
+ * @param {HTMLElement} toast
+ */
+ dismissToast(toast) {
+ if (!toast || !toast.parentNode) {
+ this.isProcessing = false;
+ this.processQueue();
+ return;
+ }
+
+ // Start fade out animation
+ toast.classList.remove('toast--show');
+ toast.classList.add('toast--hide');
+
+ // Remove from DOM after animation completes
+ setTimeout(() => {
+ if (toast.parentNode) {
+ toast.remove();
+ }
+ this.currentToast = null;
+ this.isProcessing = false;
+
+ // Process next toast in queue
+ this.processQueue();
+ }, 300); // Match CSS transition duration
+ }
+
+ /**
+ * Clear all toasts immediately
+ */
+ clearAll() {
+ this.queue = [];
+ const toasts = this.containerTarget.querySelectorAll('.toast');
+ toasts.forEach(toast => {
+ toast.remove();
+ });
+ this.currentToast = null;
+ this.isProcessing = false;
+ }
+}
+
diff --git a/assets/styles/toast.css b/assets/styles/toast.css
new file mode 100644
index 0000000..fc49c9f
--- /dev/null
+++ b/assets/styles/toast.css
@@ -0,0 +1,116 @@
+/* Toast Notification Styles */
+
+.toast-container {
+ position: fixed;
+ top: 80px; /* Below the header */
+ right: 20px;
+ z-index: 9999;
+ pointer-events: none;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ max-width: 400px;
+}
+
+.toast {
+ pointer-events: auto;
+ background: var(--color-bg-light);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ min-height: 48px;
+ border: 1px solid;
+ border-left: 4px solid;
+ opacity: 0;
+ transform: translateX(400px);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.toast--show {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.toast--hide {
+ opacity: 0;
+ transform: translateX(400px);
+}
+
+/* Toast types - using theme colors */
+.toast--success {
+ border-color: var(--color-accent-600);
+ background-color: var(--color-bg-light);
+}
+
+.toast--danger,
+.toast--error {
+ border-color: #dc3545;
+ background-color: var(--color-bg-light);
+}
+
+.toast--warning {
+ border-color: var(--color-accent-strong);
+ background-color: var(--color-bg-light);
+}
+
+.toast--info {
+ border-color: var(--color-teal-500);
+ background-color: var(--color-bg-light);
+}
+
+.toast__content {
+ flex: 1;
+ font-size: 14px;
+ line-height: 1.4;
+ color: var(--color-text);
+}
+
+.toast__close {
+ background: none;
+ border: none;
+ font-size: 24px;
+ line-height: 1;
+ color: currentColor;
+ opacity: 0.5;
+ cursor: pointer;
+ padding: 0;
+ margin-left: 12px;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.2s;
+}
+
+.toast__close:hover {
+ opacity: 0.8;
+}
+
+.toast__close:focus {
+ outline: 2px solid currentColor;
+ outline-offset: 2px;
+ opacity: 1;
+}
+
+
+/* Mobile responsive */
+@media (max-width: 768px) {
+ .toast-container {
+ top: 60px;
+ right: 10px;
+ left: 10px;
+ max-width: none;
+ }
+
+ .toast {
+ width: 100%;
+ }
+}
+
+/* Animation for stacking */
+.toast:not(:last-child) {
+ margin-bottom: 0;
+}
+
diff --git a/templates/components/Toast.html.twig b/templates/components/Toast.html.twig
new file mode 100644
index 0000000..2db8ecb
--- /dev/null
+++ b/templates/components/Toast.html.twig
@@ -0,0 +1,18 @@
+{#
+ Toast Notification Component
+
+ Include this once in your layout to enable toast notifications throughout your app.
+
+ Usage:
+ {% include 'components/Toast.html.twig' %}
+
+ Then from JavaScript:
+ window.showToast('Success!', 'success');
+ window.showToast('Error occurred', 'danger');
+ window.showToast('Processing...', 'info');
+ window.showToast('Warning!', 'warning');
+#}
+
+ {# Toast messages will be dynamically inserted here #}
+
+
diff --git a/templates/editor/layout.html.twig b/templates/editor/layout.html.twig
index a80c3bf..f47b2ae 100644
--- a/templates/editor/layout.html.twig
+++ b/templates/editor/layout.html.twig
@@ -42,6 +42,9 @@
{% endblock %}
{% block layout %}
+{# Toast notification system #}
+{% include 'components/Toast.html.twig' %}
+