diff --git a/.Jules/palette.md b/.Jules/palette.md
new file mode 100644
index 0000000..c4432f2
--- /dev/null
+++ b/.Jules/palette.md
@@ -0,0 +1,4 @@
+
+## 2026-05-29 - [Robust Async Loading States for Form Submits]
+**Learning:** When adding loading states (disabled, aria-busy, text updates) to submit buttons in Vanilla JS forms where the submit event binds to the form itself, it's critical to access the specific button via `e.submitter`. Always implement state restoration (e.g. restoring original text, enabling the button, and removing the aria-busy attribute) inside a `finally` block to ensure UI recovery regardless of whether the async logic (e.g., login, register) resolves successfully or throws an error.
+**Action:** Use `e.submitter` inside a null-check alongside `try/finally` for implementing loading states in form submit handlers where direct button clicks aren't individually targeted.
diff --git a/web-demo/css/style.css b/web-demo/css/style.css
index e0d4506..485faf5 100644
--- a/web-demo/css/style.css
+++ b/web-demo/css/style.css
@@ -1198,4 +1198,8 @@ body {
body {
padding: 0;
}
-}
\ No newline at end of file
+}
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
diff --git a/web-demo/js/app.js b/web-demo/js/app.js
index 11508de..d35a5d2 100644
--- a/web-demo/js/app.js
+++ b/web-demo/js/app.js
@@ -145,6 +145,14 @@ class ClimaAI {
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
+ let originalText = '';
+ if (e.submitter) {
+ originalText = e.submitter.innerHTML;
+ e.submitter.disabled = true;
+ e.submitter.setAttribute('aria-busy', 'true');
+ e.submitter.innerHTML = '⏳ Signing in...';
+ }
+
try {
this.showToast('Logging in...', 'info');
const response = await api.login(email, password);
@@ -155,6 +163,12 @@ class ClimaAI {
this.checkSubscription();
} catch (error) {
this.showToast(error.message || 'Login failed', 'error');
+ } finally {
+ if (e.submitter) {
+ e.submitter.disabled = false;
+ e.submitter.removeAttribute('aria-busy');
+ e.submitter.innerHTML = originalText;
+ }
}
}
@@ -164,6 +178,14 @@ class ClimaAI {
const email = document.getElementById('registerEmail').value;
const password = document.getElementById('registerPassword').value;
+ let originalText = '';
+ if (e.submitter) {
+ originalText = e.submitter.innerHTML;
+ e.submitter.disabled = true;
+ e.submitter.setAttribute('aria-busy', 'true');
+ e.submitter.innerHTML = '⏳ Signing up...';
+ }
+
try {
this.showToast('Creating account...', 'info');
const response = await api.register(email, password, name);
@@ -174,6 +196,12 @@ class ClimaAI {
this.checkSubscription();
} catch (error) {
this.showToast(error.message || 'Registration failed', 'error');
+ } finally {
+ if (e.submitter) {
+ e.submitter.disabled = false;
+ e.submitter.removeAttribute('aria-busy');
+ e.submitter.innerHTML = originalText;
+ }
}
}