← back to Big Red
feat(widget): avatar launcher with red->green live state on click
09a1667e99397e02c9ab80bf949b34bac41de327 · 2026-05-11 23:52:22 -0700 · Steve Abrams
Replaces the BIG/RED text glyph with Steve's circular avatar headshot
(220px PNG seeded from agent-avatars/steve.png at public/avatar/steve.jpg
— matches the /api/avatar exists path).
Visual state machine:
- Closed: red ring + red-pulse animation + red wash overlay (mix-blend
multiply) so the avatar reads tinted but recognizable
- Hover: scales 1.08
- Open ('live'): green ring + green-pulse + green wash overlay; tag pill
next to the launcher swaps from 'TALK TO BIG RED' (red) to 'LIVE — TAP
TO HIDE' (green). Live indicator dot appears in the iframe title bar.
- Iframe loads /?embed=1&voice=1 so Big Red can default to voice mode
Falls back gracefully to 'BR' monogram if the avatar 404s, so the widget
still renders on cold installs before the user uploads a face.
Files touched
A public/avatar/steve.jpgM public/widget.js
Diff
commit 09a1667e99397e02c9ab80bf949b34bac41de327
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 11 23:52:22 2026 -0700
feat(widget): avatar launcher with red->green live state on click
Replaces the BIG/RED text glyph with Steve's circular avatar headshot
(220px PNG seeded from agent-avatars/steve.png at public/avatar/steve.jpg
— matches the /api/avatar exists path).
Visual state machine:
- Closed: red ring + red-pulse animation + red wash overlay (mix-blend
multiply) so the avatar reads tinted but recognizable
- Hover: scales 1.08
- Open ('live'): green ring + green-pulse + green wash overlay; tag pill
next to the launcher swaps from 'TALK TO BIG RED' (red) to 'LIVE — TAP
TO HIDE' (green). Live indicator dot appears in the iframe title bar.
- Iframe loads /?embed=1&voice=1 so Big Red can default to voice mode
Falls back gracefully to 'BR' monogram if the avatar 404s, so the widget
still renders on cold installs before the user uploads a face.
---
public/avatar/steve.jpg | Bin 0 -> 81865 bytes
public/widget.js | 224 ++++++++++++++++++++++++++++++++----------------
2 files changed, 148 insertions(+), 76 deletions(-)
diff --git a/public/avatar/steve.jpg b/public/avatar/steve.jpg
new file mode 100644
index 0000000..fbc9b84
Binary files /dev/null and b/public/avatar/steve.jpg differ
diff --git a/public/widget.js b/public/widget.js
index 34136d0..83e736d 100644
--- a/public/widget.js
+++ b/public/widget.js
@@ -1,16 +1,13 @@
-// Big Red embed widget — drops a floating launcher into the lower-left of the
-// host page. Click → expands to an iframe of the Big Red chat UI.
+// Big Red embed widget — drops a circular avatar launcher into the lower-left
+// of the host page. Click → expands to an iframe of the Big Red chat UI.
+// The launcher avatar is RED when closed and turns GREEN when open (live chat).
//
// Usage on a host page:
// <script src="https://<big-red-host>/widget.js"
// data-host="https://<big-red-host>"
// defer></script>
//
-// Or, if same-origin / dev:
-// <script src="/widget.js" data-host="http://localhost:9935" defer></script>
-//
-// The script reads its own `src` to derive the default host if data-host is
-// omitted. Idempotent — safe to load twice; only one launcher is mounted.
+// Idempotent — safe to load twice; only one launcher is mounted.
(function () {
if (window.__bigRedMounted) return;
window.__bigRedMounted = true;
@@ -27,59 +24,109 @@
left: 22px;
bottom: 22px;
z-index: 99990;
- width: 60px;
- height: 60px;
+ width: 64px;
+ height: 64px;
border-radius: 50%;
- background:
- radial-gradient(circle at 35% 30%, #ff5560 0%, #d8323a 45%, #8b1419 100%);
- box-shadow:
- 0 0 0 2px rgba(255,255,255,.08),
- 0 8px 28px rgba(216,50,58,.55),
- 0 2px 10px rgba(0,0,0,.4);
border: 0;
+ padding: 0;
cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", sans-serif;
- font-weight: 900;
- letter-spacing: 0.06em;
- color: #fff;
- font-size: 12px;
- text-shadow: 0 0 8px rgba(0,0,0,.5);
- transition: transform .18s cubic-bezier(.2,.7,.2,1), box-shadow .18s, opacity .18s;
- animation: bigred-pulse 2.6s ease-in-out infinite;
- }
- .bigred-launcher:hover {
- transform: scale(1.08);
+ background: #1a0506;
+ overflow: hidden;
box-shadow:
- 0 0 0 3px rgba(255,255,255,.14),
- 0 12px 40px rgba(216,50,58,.7),
- 0 2px 12px rgba(0,0,0,.5);
+ 0 0 0 3px #d8323a,
+ 0 0 0 5px rgba(216,50,58,.18),
+ 0 8px 24px rgba(216,50,58,.5),
+ 0 2px 10px rgba(0,0,0,.4);
+ transition: transform .18s cubic-bezier(.2,.7,.2,1), box-shadow .25s ease;
+ animation: bigred-pulse-red 2.6s ease-in-out infinite;
+ }
+ .bigred-launcher img {
+ width: 100%; height: 100%; display: block;
+ object-fit: cover; object-position: center 28%;
+ filter: saturate(.6) brightness(.92);
+ transition: filter .25s ease;
}
- .bigred-launcher:active { transform: scale(0.96); }
- .bigred-launcher.open { opacity: 0; pointer-events: none; }
- .bigred-launcher .label {
+ .bigred-launcher::after {
+ /* red wash overlay (closed state) */
+ content: '';
+ position: absolute; inset: 0;
+ border-radius: 50%;
+ background: radial-gradient(circle at 35% 30%, rgba(255,85,96,.18) 0%, rgba(139,20,25,.42) 65%, rgba(58,10,12,.55) 100%);
+ mix-blend-mode: multiply;
+ transition: opacity .28s ease, background .28s ease;
pointer-events: none;
- letter-spacing: 0.14em;
}
- @keyframes bigred-pulse {
+ .bigred-launcher:hover { transform: scale(1.08); }
+ .bigred-launcher:active { transform: scale(0.94); }
+
+ .bigred-launcher.live {
+ box-shadow:
+ 0 0 0 3px #2fbf4a,
+ 0 0 0 5px rgba(47,191,74,.20),
+ 0 8px 28px rgba(47,191,74,.55),
+ 0 2px 10px rgba(0,0,0,.4);
+ animation: bigred-pulse-green 1.8s ease-in-out infinite;
+ }
+ .bigred-launcher.live img { filter: saturate(.85) brightness(1); }
+ .bigred-launcher.live::after {
+ background: radial-gradient(circle at 35% 30%, rgba(120,255,140,.22) 0%, rgba(40,160,70,.30) 65%, rgba(20,80,40,.42) 100%);
+ }
+
+ @keyframes bigred-pulse-red {
0%, 100% { box-shadow:
- 0 0 0 2px rgba(255,255,255,.08),
- 0 8px 28px rgba(216,50,58,.55),
+ 0 0 0 3px #d8323a,
+ 0 0 0 5px rgba(216,50,58,.18),
+ 0 8px 24px rgba(216,50,58,.5),
0 2px 10px rgba(0,0,0,.4); }
50% { box-shadow:
- 0 0 0 2px rgba(255,255,255,.14),
- 0 8px 38px rgba(255,85,96,.78),
- 0 2px 14px rgba(0,0,0,.45); }
+ 0 0 0 3px #ff5560,
+ 0 0 0 7px rgba(255,85,96,.22),
+ 0 8px 32px rgba(255,85,96,.7),
+ 0 2px 12px rgba(0,0,0,.45); }
}
+ @keyframes bigred-pulse-green {
+ 0%, 100% { box-shadow:
+ 0 0 0 3px #2fbf4a,
+ 0 0 0 5px rgba(47,191,74,.20),
+ 0 8px 24px rgba(47,191,74,.5),
+ 0 2px 10px rgba(0,0,0,.4); }
+ 50% { box-shadow:
+ 0 0 0 3px #58e070,
+ 0 0 0 8px rgba(88,224,112,.26),
+ 0 8px 32px rgba(88,224,112,.72),
+ 0 2px 12px rgba(0,0,0,.45); }
+ }
+
+ .bigred-tag {
+ position: fixed;
+ left: 96px;
+ bottom: 36px;
+ z-index: 99990;
+ pointer-events: none;
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", sans-serif;
+ font-weight: 900;
+ letter-spacing: 0.22em;
+ font-size: 10px;
+ color: #ff5560;
+ text-shadow: 0 0 12px rgba(255,85,96,.55), 0 0 2px rgba(0,0,0,.6);
+ opacity: 0;
+ transform: translateX(-6px);
+ transition: opacity .25s, transform .25s, color .25s, text-shadow .25s;
+ }
+ .bigred-launcher:hover + .bigred-tag,
+ .bigred-launcher.live + .bigred-tag { opacity: 1; transform: translateX(0); }
+ .bigred-launcher.live + .bigred-tag {
+ color: #58e070;
+ text-shadow: 0 0 12px rgba(88,224,112,.55), 0 0 2px rgba(0,0,0,.6);
+ }
+
.bigred-panel {
position: fixed;
left: 22px;
- bottom: 22px;
+ bottom: 96px;
z-index: 99991;
- width: min(420px, calc(100vw - 44px));
- height: min(640px, calc(100vh - 44px));
+ width: min(440px, calc(100vw - 44px));
+ height: min(660px, calc(100vh - 130px));
background: #1a0506;
border: 1px solid #8b1419;
border-radius: 18px;
@@ -113,34 +160,33 @@
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", sans-serif;
font-size: 11px;
font-weight: 900;
- letter-spacing: 0.18em;
+ letter-spacing: 0.22em;
text-shadow: 0 0 12px rgba(255,85,96,.5);
}
+ .bigred-bar .live-dot {
+ display: inline-block; width: 7px; height: 7px; border-radius: 50%;
+ background: #58e070; margin-right: 8px; vertical-align: middle;
+ box-shadow: 0 0 8px #58e070;
+ animation: bigred-livedot 1.4s ease-in-out infinite;
+ }
+ @keyframes bigred-livedot {
+ 0%, 100% { opacity: 1; transform: scale(1); }
+ 50% { opacity: 0.55; transform: scale(.85); }
+ }
.bigred-bar .close {
- background: transparent;
- border: 0;
- color: #ff5560;
- cursor: pointer;
- width: 26px;
- height: 26px;
- border-radius: 6px;
- font-size: 18px;
- line-height: 1;
- display: flex;
- align-items: center;
- justify-content: center;
+ background: transparent; border: 0; color: #ff5560; cursor: pointer;
+ width: 26px; height: 26px; border-radius: 6px; font-size: 18px; line-height: 1;
+ display: flex; align-items: center; justify-content: center;
transition: background .12s;
}
.bigred-bar .close:hover { background: rgba(255,85,96,.16); }
.bigred-frame {
- flex: 1;
- width: 100%;
- border: 0;
- background: #1a0506;
+ flex: 1; width: 100%; border: 0; background: #1a0506;
}
@media (max-width: 640px) {
- .bigred-launcher { left: 16px; bottom: 16px; width: 52px; height: 52px; font-size: 10px; }
- .bigred-panel { left: 8px; right: 8px; bottom: 8px; width: auto; }
+ .bigred-launcher { left: 16px; bottom: 16px; width: 56px; height: 56px; }
+ .bigred-tag { left: 80px; bottom: 28px; font-size: 9px; }
+ .bigred-panel { left: 8px; right: 8px; bottom: 84px; width: auto; }
}
`;
@@ -152,25 +198,43 @@
const launcher = document.createElement('button');
launcher.type = 'button';
launcher.className = 'bigred-launcher';
- launcher.setAttribute('aria-label', 'Open Big Red chat');
- launcher.innerHTML = '<span class="label">BIG<br>RED</span>';
+ launcher.setAttribute('aria-label', 'Open Big Red voice chat');
+ // Avatar image — falls back to Steve's stored avatar at /avatar/steve.jpg.
+ // Add a query bust so updated frames refresh on next page load.
+ const avatarImg = document.createElement('img');
+ avatarImg.alt = '';
+ avatarImg.src = host + '/avatar/steve.jpg?ts=' + Date.now();
+ avatarImg.onerror = () => {
+ // No avatar uploaded yet — fall back to a "BR" monogram glyph.
+ avatarImg.style.display = 'none';
+ const mono = document.createElement('div');
+ mono.style.cssText = 'width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-family:-apple-system,sans-serif;font-weight:900;font-size:14px;letter-spacing:.04em;color:#fff;text-shadow:0 0 8px rgba(0,0,0,.6);background:#5a0d10;';
+ mono.textContent = 'BR';
+ launcher.appendChild(mono);
+ };
+ launcher.appendChild(avatarImg);
+
+ const tag = document.createElement('div');
+ tag.className = 'bigred-tag';
+ tag.textContent = 'TALK TO BIG RED';
const panel = document.createElement('div');
panel.className = 'bigred-panel';
panel.setAttribute('role', 'dialog');
- panel.setAttribute('aria-label', 'Big Red chat');
+ panel.setAttribute('aria-label', 'Big Red voice chat');
panel.innerHTML = `
<div class="bigred-bar">
- <span>BIG RED</span>
+ <span><span class="live-dot"></span>BIG RED · VOICE</span>
<button class="close" aria-label="Close">×</button>
</div>
- <iframe class="bigred-frame" title="Big Red chat"
- allow="microphone; camera; clipboard-read; clipboard-write"
+ <iframe class="bigred-frame" title="Big Red voice chat"
+ allow="microphone; camera; clipboard-read; clipboard-write; autoplay"
src="about:blank"
loading="lazy"></iframe>
`;
document.body.appendChild(launcher);
+ document.body.appendChild(tag);
document.body.appendChild(panel);
const iframe = panel.querySelector('.bigred-frame');
@@ -178,16 +242,24 @@
function open() {
if (iframe.src === 'about:blank' || !iframe.src.startsWith(host)) {
- iframe.src = host + '/?embed=1';
+ // ?embed=1&voice=1 — host UI can pick this up to default to voice mode.
+ iframe.src = host + '/?embed=1&voice=1';
}
- launcher.classList.add('open');
+ launcher.classList.add('live');
+ launcher.setAttribute('aria-label', 'Close Big Red (currently live)');
+ tag.textContent = 'LIVE — TAP TO HIDE';
panel.classList.add('open');
}
function close() {
- launcher.classList.remove('open');
+ launcher.classList.remove('live');
+ launcher.setAttribute('aria-label', 'Open Big Red voice chat');
+ tag.textContent = 'TALK TO BIG RED';
panel.classList.remove('open');
}
- launcher.addEventListener('click', open);
+ function toggle() {
+ if (panel.classList.contains('open')) close(); else open();
+ }
+ launcher.addEventListener('click', toggle);
closeBtn.addEventListener('click', close);
// Esc closes the panel
@@ -195,6 +267,6 @@
if (e.key === 'Escape' && panel.classList.contains('open')) close();
});
- // Expose a tiny API so host pages can toggle programmatically if they want
- window.BigRed = { open, close, host };
+ // Public API
+ window.BigRed = { open, close, toggle, host };
})();
← 83c3c52 feat(big-red): embeddable widget.js — floating launcher for
·
back to Big Red
·
widget: anchor bottom-right; slide launcher left of panel wh 74925bb →