From 6f339a46d4ead529f4f113e66a3b574f50d8eb51 Mon Sep 17 00:00:00 2001 From: Loom Date: Sun, 14 Jun 2026 15:30:06 -0500 Subject: [PATCH 1/3] feat(composer): add emoji picker and browser mic STT Add two new buttons to the onscreen agent composer toolbar (full mode only): **Emoji picker** - Smiley face button opens a popover with 90+ emoji across 7 categories - Picker is positioned via the existing positionPopover() utility, matching the composerActionMenu pattern (anchor-aware, top/end alignment) - Emoji inserts at cursor position with correct UTF-16 cursor advancement - Teleported to body via x-teleport to escape stacking contexts - Closes on outside click, Escape, or after insertion - Button shows active state while picker is open **Browser mic / STT** - Mic button uses the Web Speech API (webkitSpeechRecognition / SpeechRecognition) - Button hidden automatically when the API is not available (Firefox, etc.) - Click to start, auto-stops after speech pause; click again to cancel - Transcript appended to draft with smart spacing - Pulsing red animation while recording **Implementation notes** - All state lives in the Alpine store (model object): emojiPickerAnchor, emojiPickerPosition, isEmojiPickerVisible, isRecording, sttSupported, _sttRecognition - No external dependencies; CSS uses existing space design tokens with sensible hard-coded fallbacks - Tested in Chrome (full functionality) and Firefox (emoji only, mic button correctly hidden); Brave blocks the STT network request by default --- .../_core/onscreen_agent/onscreen-agent.css | 56 ++++++ .../_all/mod/_core/onscreen_agent/panel.html | 52 ++++++ app/L0/_all/mod/_core/onscreen_agent/store.js | 170 ++++++++++++++++++ 3 files changed, 278 insertions(+) diff --git a/app/L0/_all/mod/_core/onscreen_agent/onscreen-agent.css b/app/L0/_all/mod/_core/onscreen_agent/onscreen-agent.css index 85d180ff..181b1fa7 100644 --- a/app/L0/_all/mod/_core/onscreen_agent/onscreen-agent.css +++ b/app/L0/_all/mod/_core/onscreen_agent/onscreen-agent.css @@ -1104,3 +1104,59 @@ max-width: calc(100% - 46px); } } + +/* ── Mic button ─────────────────────────────────────────────────────────── */ +.onscreen-agent-mic-button.is-recording { + color: var(--space-danger, #f87171); + background: color-mix(in srgb, var(--space-danger, #f87171) 12%, transparent); + animation: onscreen-agent-mic-pulse 1.2s ease-in-out infinite; +} + +@keyframes onscreen-agent-mic-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +/* ── Emoji button active state ───────────────────────────────────────────── */ +.onscreen-agent-emoji-button.is-active { + color: var(--space-accent, var(--chat-send-background, #818cf8)); + background: color-mix(in srgb, var(--space-accent, #818cf8) 12%, transparent); +} + +/* ── Emoji picker popover ────────────────────────────────────────────────── */ +.onscreen-agent-emoji-picker { + position: fixed; + background: var(--space-surface-2, var(--chat-surface, #1e1e2e)); + border: 1px solid var(--space-border, rgba(255, 255, 255, 0.13)); + border-radius: 12px; + padding: 10px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55); + z-index: var(--space-popover-z, 9000); + width: 268px; + max-height: 320px; + overflow-y: auto; +} + +.onscreen-agent-emoji-grid { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 2px; +} + +.onscreen-agent-emoji-item { + background: none; + border: none; + cursor: pointer; + font-size: 20px; + line-height: 1; + padding: 5px 3px; + border-radius: 6px; + transition: background 0.1s; + display: flex; + align-items: center; + justify-content: center; +} + +.onscreen-agent-emoji-item:hover { + background: var(--space-surface-hover, rgba(255, 255, 255, 0.1)); +} diff --git a/app/L0/_all/mod/_core/onscreen_agent/panel.html b/app/L0/_all/mod/_core/onscreen_agent/panel.html index a4510bdc..fb35c749 100644 --- a/app/L0/_all/mod/_core/onscreen_agent/panel.html +++ b/app/L0/_all/mod/_core/onscreen_agent/panel.html @@ -194,6 +194,33 @@ > attach_file + +