From a263f40178483e9d3a3cf202fd890c602f3c3e77 Mon Sep 17 00:00:00 2001 From: nmemmert Date: Tue, 28 Jul 2026 12:15:45 -0400 Subject: [PATCH] Fix compose focus steal; add contact picker to To field; v1.1.17 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Focus effect now only fires when compose/reply first opens, not on every keystroke — typing in the To field no longer jumps focus to the body - 👥 button next to To field opens a searchable contact picker dropdown; selecting a contact fills in email and name instantly Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- src/App.css | 100 ++++++++++++++++++++++++++++++++++++++++++++++ src/EmailPage.tsx | 74 +++++++++++++++++++++++++++++----- 3 files changed, 165 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index e984f9b..1f2e0c0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "1.1.16", + "version": "1.1.17", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.css b/src/App.css index dfaff62..70122e9 100644 --- a/src/App.css +++ b/src/App.css @@ -9765,6 +9765,106 @@ border-color: rgba(201, 168, 76, 0.45); } +.em-to-field-wrap { + flex: 1; + position: relative; + display: flex; + align-items: center; + gap: 0.4rem; + min-width: 0; +} + +.em-to-field-wrap .em-compose-input { + flex: 1; + min-width: 0; +} + +.em-contact-pick-btn { + background: rgba(255,255,255,0.06); + border: 1px solid rgba(201,168,76,0.2); + border-radius: 6px; + color: #c8b898; + font-size: 0.9rem; + padding: 0.3rem 0.45rem; + cursor: pointer; + flex-shrink: 0; + line-height: 1; + transition: background 0.12s; +} +.em-contact-pick-btn:hover { background: rgba(255,255,255,0.1); } + +.em-contact-picker { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 36px; + background: #1e1c16; + border: 1px solid rgba(201,168,76,0.3); + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0,0,0,0.6); + z-index: 50; + overflow: hidden; +} + +.em-contact-picker-search { + width: 100%; + box-sizing: border-box; + background: transparent; + border: none; + border-bottom: 1px solid rgba(201,168,76,0.15); + padding: 0.5rem 0.75rem; + color: #f0ead8; + font-size: 0.85rem; + font-family: system-ui, sans-serif; + outline: none; +} + +.em-contact-picker-list { + max-height: 220px; + overflow-y: auto; +} + +.em-contact-picker-empty { + padding: 0.6rem 0.75rem; + color: #6a6050; + font-size: 0.83rem; +} + +.em-contact-picker-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + width: 100%; + padding: 0.45rem 0.75rem; + background: none; + border: none; + cursor: pointer; + text-align: left; + transition: background 0.1s; +} +.em-contact-picker-item:hover { background: rgba(255,255,255,0.06); } + +.em-contact-picker-name { + font-size: 0.85rem; + color: #e8e2d5; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex-shrink: 0; + max-width: 45%; +} + +.em-contact-picker-email { + font-size: 0.78rem; + color: #c8860a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + text-align: right; +} + .em-compose-select { flex: 1; background: #14130f; diff --git a/src/EmailPage.tsx b/src/EmailPage.tsx index 720cb6b..f922059 100644 --- a/src/EmailPage.tsx +++ b/src/EmailPage.tsx @@ -130,6 +130,8 @@ function EmailClient({ onLogout }: { onLogout: () => void }) { const [settingsSaving, setSettingsSaving] = useState(false) const [settingsSaved, setSettingsSaved] = useState(false) const [actionMsg, setActionMsg] = useState('') + const [showContactPicker, setShowContactPicker] = useState(false) + const [contactPickerSearch, setContactPickerSearch] = useState('') const composeRef = useRef(null) const listRef = useRef(null) @@ -233,9 +235,14 @@ function EmailClient({ onLogout }: { onLogout: () => void }) { el?.scrollIntoView({ block: 'nearest' }) }, [selectedId]) - // Focus compose textarea when reply opens + // Focus compose textarea only when reply/compose first opens (not on every keystroke) + const hadDraftRef = useRef(false) useEffect(() => { - if (replyDraft) setTimeout(() => composeRef.current?.focus(), 50) + const hasDraft = Boolean(replyDraft) + if (hasDraft && !hadDraftRef.current) { + setTimeout(() => composeRef.current?.focus(), 50) + } + hadDraftRef.current = hasDraft }, [replyDraft]) // ── Keyboard nav ── @@ -504,13 +511,60 @@ function EmailClient({ onLogout }: { onLogout: () => void }) {
- setReplyDraft({ ...replyDraft, recipientEmail: e.target.value })} - /> +
+ setReplyDraft({ ...replyDraft, recipientEmail: e.target.value })} + /> + +
+ {showContactPicker && (() => { + const q = contactPickerSearch.toLowerCase() + const seen = new Set() + const opts = submissions + .filter(s => s.email && !seen.has(s.email) && seen.add(s.email) as unknown as boolean) + .filter(s => !q || s.email.toLowerCase().includes(q) || (s.name || '').toLowerCase().includes(q)) + .slice(0, 40) + return ( +
+ setContactPickerSearch(e.target.value)} + /> +
+ {opts.length === 0 &&
No contacts found
} + {opts.map(s => ( + + ))} +
+
+ ) + })()}
@@ -566,7 +620,7 @@ function EmailClient({ onLogout }: { onLogout: () => void }) { - +
{replyMsg &&

{replyMsg}

}