wip autocomplete

This commit is contained in:
vineyardbovines
2026-04-14 14:24:02 -04:00
parent 127d214de5
commit 794ffd474a
8 changed files with 1259 additions and 0 deletions
@@ -0,0 +1,173 @@
<h2>GIF Autocomplete — Full Design</h2>
<p class="subtitle">Inline search suggestions powered by Klipy's autocomplete endpoint</p>
<div class="section">
<h3>Architecture Overview</h3>
<div class="mockup">
<div class="mockup-header">Component & Data Flow</div>
<div class="mockup-body" style="padding:24px;font-family:system-ui;font-size:14px;line-height:1.6">
<div style="display:flex;gap:32px;flex-wrap:wrap">
<!-- Left: component tree -->
<div style="flex:1;min-width:300px">
<div style="font-weight:600;margin-bottom:12px;color:#7c7cff;text-transform:uppercase;font-size:11px;letter-spacing:1px">Component Tree</div>
<div style="font-family:monospace;font-size:13px;line-height:2">
<div>GifPickerDialog</div>
<div style="padding-left:20px;opacity:0.6">└─ GifPickerBody</div>
<div style="padding-left:40px">├─ <span style="color:#7c7cff">GifPickerHeader</span></div>
<div style="padding-left:60px">│ ├─ TextField.Input <span style="opacity:0.5">(search box)</span></div>
<div style="padding-left:60px">│ └─ <span style="color:#5cdb5c;font-weight:600">GifAutocompleteSuggestions</span> <span style="color:#5cdb5c;opacity:0.7">← NEW</span></div>
<div style="padding-left:40px">├─ GifPickerPlaceholder</div>
<div style="padding-left:40px">└─ GifPickerGrid</div>
</div>
</div>
<!-- Right: data flow -->
<div style="flex:1;min-width:300px">
<div style="font-weight:600;margin-bottom:12px;color:#ff7c7c;text-transform:uppercase;font-size:11px;letter-spacing:1px">Data Flow</div>
<div style="font-family:monospace;font-size:13px;line-height:2">
<div><span style="opacity:0.5">1.</span> User types → rawSearch state</div>
<div><span style="opacity:0.5">2.</span> rawSearch throttled @ <span style="color:#5cdb5c">200ms</span> → autocomplete query</div>
<div><span style="opacity:0.5">3.</span> rawSearch throttled @ 500ms → GIF search query</div>
<div><span style="opacity:0.5">4.</span> Autocomplete response → suggestion strings</div>
<div><span style="opacity:0.5">5.</span> User taps suggestion → fills input + fires search</div>
<div><span style="opacity:0.5">6.</span> Suggestions hide when input matches a selected term</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h3>New Files</h3>
<div style="font-family:monospace;font-size:13px;line-height:2.2;padding:0 8px">
<div><span style="color:#5cdb5c">+</span> <strong>src/lib/constants.ts</strong> — add <code>GIF_KLIPY_AUTOCOMPLETE</code> endpoint</div>
<div><span style="color:#5cdb5c">+</span> <strong>src/state/queries/klipy.ts</strong> — add <code>useKlipyAutocompleteQuery</code> hook</div>
<div><span style="color:#5cdb5c">+</span> <strong>src/features/gifPicker/hooks/useGifAutocomplete.ts</strong> — orchestration hook (throttle + query + keyboard state)</div>
<div><span style="color:#5cdb5c">+</span> <strong>src/features/gifPicker/components/GifAutocompleteSuggestions.tsx</strong> — inline suggestion list UI</div>
<div><span style="color:#ffcc00">~</span> <strong>src/features/gifPicker/components/GifPickerHeader.tsx</strong> — render suggestions below input</div>
<div><span style="color:#ffcc00">~</span> <strong>src/features/gifPicker/GifPickerDialog.tsx</strong> — wire up autocomplete state</div>
</div>
</div>
<div class="section">
<h3>Interaction Flow</h3>
<div style="display:flex;gap:16px;flex-wrap:wrap">
<!-- State 1: Idle -->
<div style="flex:1;min-width:220px">
<div class="mockup">
<div class="mockup-header">1. Idle — no suggestions</div>
<div class="mockup-body" style="padding:12px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0;font-size:13px">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="opacity:0.4">&#128269;</span>
<span style="opacity:0.4">Search GIFs</span>
</div>
<div style="display:flex;gap:6px">
<div style="flex:1;background:#333;border-radius:6px;height:50px;opacity:0.4"></div>
<div style="flex:1;background:#333;border-radius:6px;height:40px;opacity:0.4"></div>
</div>
<div style="text-align:center;margin-top:8px;font-size:11px;opacity:0.5">Featured GIFs</div>
</div>
</div>
</div>
<!-- State 2: Typing, suggestions visible -->
<div style="flex:1;min-width:220px">
<div class="mockup">
<div class="mockup-header">2. Typing — suggestions appear</div>
<div class="mockup-body" style="padding:12px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0;font-size:13px">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span style="opacity:0.4">&#128269;</span>
<span>hap<span style="opacity:0.3">|</span></span>
</div>
<div style="background:#22223a;border-radius:6px;margin-bottom:8px;overflow:hidden;border:1px solid #333">
<div style="padding:7px 10px;background:#2a2a5a;display:flex;align-items:center;gap:6px;font-size:12px">
<span style="opacity:0.3">&#128269;</span> happy birthday
</div>
<div style="padding:7px 10px;display:flex;align-items:center;gap:6px;font-size:12px;border-top:1px solid #2a2a3a">
<span style="opacity:0.3">&#128269;</span> happy
</div>
<div style="padding:7px 10px;display:flex;align-items:center;gap:6px;font-size:12px;border-top:1px solid #2a2a3a">
<span style="opacity:0.3">&#128269;</span> happy easter
</div>
</div>
<div style="display:flex;gap:6px;opacity:0.3">
<div style="flex:1;background:#333;border-radius:6px;height:30px"></div>
<div style="flex:1;background:#333;border-radius:6px;height:30px"></div>
</div>
</div>
</div>
</div>
<!-- State 3: Selected, search fires -->
<div style="flex:1;min-width:220px">
<div class="mockup">
<div class="mockup-header">3. Selected — GIFs load</div>
<div class="mockup-body" style="padding:12px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0;font-size:13px">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="opacity:0.4">&#128269;</span>
<span>happy birthday</span>
<span style="margin-left:auto;opacity:0.4;font-size:11px">&#10005;</span>
</div>
<div style="display:flex;gap:6px">
<div style="flex:1;display:flex;flex-direction:column;gap:6px">
<div style="background:#444;border-radius:6px;height:50px"></div>
<div style="background:#444;border-radius:6px;height:40px"></div>
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:6px">
<div style="background:#444;border-radius:6px;height:40px"></div>
<div style="background:#444;border-radius:6px;height:50px"></div>
</div>
</div>
<div style="text-align:center;margin-top:8px;font-size:11px;opacity:0.5">Results for "happy birthday"</div>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h3>Keyboard Navigation (Web)</h3>
<div style="font-family:monospace;font-size:13px;line-height:2;padding:0 8px">
<div><strong>ArrowDown / ArrowUp</strong> — move active highlight through suggestions</div>
<div><strong>Enter</strong> — select highlighted suggestion (fills input, fires search)</div>
<div><strong>Escape</strong> — dismiss suggestions (existing behavior closes dialog)</div>
<div><strong>Continue typing</strong> — resets active index to 0, re-fetches suggestions</div>
</div>
<p style="margin-top:8px;font-size:13px;opacity:0.7">
ARIA: input gets <code>role="combobox"</code> + <code>aria-controls</code>, list gets <code>role="listbox"</code>,
items get <code>role="option"</code> + <code>aria-selected</code>. ~30 lines of <code>onKeyDown</code> handling on web only.
</p>
</div>
<div class="section">
<h3>Visibility Rules</h3>
<div style="font-family:monospace;font-size:13px;line-height:2;padding:0 8px">
<div><span style="color:#5cdb5c">SHOW</span> when: rawSearch.length ≥ 1 AND suggestions.length > 0 AND user hasn't just selected a suggestion</div>
<div><span style="color:#ff7c7c">HIDE</span> when: rawSearch is empty OR user selected a suggestion OR user clears input</div>
<div><span style="color:#ffcc00">RE-SHOW</span> when: user edits the input after a selection (e.g., backspace to modify)</div>
</div>
</div>
<div class="section">
<h3>Scope Boundaries</h3>
<div class="pros-cons">
<div class="pros">
<h4>In Scope</h4>
<ul>
<li>Klipy autocomplete endpoint integration</li>
<li>Inline suggestion list in GifPickerHeader</li>
<li>200ms throttle for autocomplete, independent of 500ms search throttle</li>
<li>Keyboard navigation on web (arrow keys, enter, escape)</li>
<li>ARIA accessibility attributes</li>
<li>Tap-to-select on native</li>
</ul>
</div>
<div class="cons">
<h4>Out of Scope</h4>
<ul>
<li>Tenor autocomplete (Klipy only)</li>
<li>Trending/popular suggestions on empty input</li>
<li>Search history / recents</li>
<li>Categories or tag browsing</li>
</ul>
</div>
</div>
</div>
@@ -0,0 +1,149 @@
<h2>Where should autocomplete suggestions appear?</h2>
<p class="subtitle">As you type in the GIF search box, where do completion suggestions show up?</p>
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Inline list below the search input</h3>
<p>Suggestions render as a vertical list between the search bar and the GIF grid. Tapping a suggestion fills the search field and triggers a GIF search. Simple, no z-index or portal concerns inside the dialog.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">GIF Dialog — Inline Suggestions</div>
<div class="mockup-body" style="padding:16px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;flex:1;display:flex;align-items:center;gap:8px">
<span style="opacity:0.5">&#128269;</span>
<span>hap<span style="opacity:0.4;border-right:2px solid #7c7cff">|</span></span>
</div>
</div>
<div style="background:#22223a;border-radius:8px;margin-bottom:12px;overflow:hidden">
<div style="padding:10px 14px;border-bottom:1px solid #333;display:flex;align-items:center;gap:8px;cursor:pointer">
<span style="opacity:0.4">&#128269;</span> <span>happy birthday</span>
</div>
<div style="padding:10px 14px;border-bottom:1px solid #333;display:flex;align-items:center;gap:8px;cursor:pointer">
<span style="opacity:0.4">&#128269;</span> <span>happy</span>
</div>
<div style="padding:10px 14px;border-bottom:1px solid #333;display:flex;align-items:center;gap:8px;cursor:pointer">
<span style="opacity:0.4">&#128269;</span> <span>happy easter</span>
</div>
<div style="padding:10px 14px;display:flex;align-items:center;gap:8px;cursor:pointer">
<span style="opacity:0.4">&#128269;</span> <span>happy dance</span>
</div>
</div>
<div style="display:flex;gap:8px">
<div style="flex:1;display:flex;flex-direction:column;gap:8px">
<div style="background:#333;border-radius:8px;height:80px;opacity:0.3"></div>
<div style="background:#333;border-radius:8px;height:60px;opacity:0.3"></div>
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:8px">
<div style="background:#333;border-radius:8px;height:60px;opacity:0.3"></div>
<div style="background:#333;border-radius:8px;height:80px;opacity:0.3"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="option" data-choice="b" onclick="toggleSelect(this)">
<div class="letter">B</div>
<div class="content">
<h3>Horizontal chips below the search input</h3>
<p>Suggestions render as a scrollable row of pill/chip buttons. Compact, leaves more vertical space for the GIF grid. Common in mobile search UIs (YouTube, Google Images).</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">GIF Dialog — Chip Suggestions</div>
<div class="mockup-body" style="padding:16px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;flex:1;display:flex;align-items:center;gap:8px">
<span style="opacity:0.5">&#128269;</span>
<span>hap<span style="opacity:0.4;border-right:2px solid #7c7cff">|</span></span>
</div>
</div>
<div style="display:flex;gap:8px;margin-bottom:12px;overflow-x:auto;padding-bottom:4px">
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">happy birthday</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">happy</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">happy easter</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">happy dance</div>
</div>
<div style="display:flex;gap:8px">
<div style="flex:1;display:flex;flex-direction:column;gap:8px">
<div style="background:#333;border-radius:8px;height:90px;opacity:0.3"></div>
<div style="background:#333;border-radius:8px;height:70px;opacity:0.3"></div>
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:8px">
<div style="background:#333;border-radius:8px;height:70px;opacity:0.3"></div>
<div style="background:#333;border-radius:8px;height:90px;opacity:0.3"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="option" data-choice="c" onclick="toggleSelect(this)">
<div class="letter">C</div>
<div class="content">
<h3>Sift popover dropdown (like Composer autocomplete)</h3>
<p>Uses the <code>@bsky.app/sift</code> library to show a positioned popover below the input, matching how @mentions work in the Composer. Keyboard navigable on web. Needs Portal/z-index verification inside the dialog.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">GIF Dialog — Sift Popover</div>
<div class="mockup-body" style="padding:16px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0">
<div style="position:relative">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;flex:1;display:flex;align-items:center;gap:8px">
<span style="opacity:0.5">&#128269;</span>
<span>hap<span style="opacity:0.4;border-right:2px solid #7c7cff">|</span></span>
</div>
</div>
<div style="position:absolute;top:40px;left:0;right:0;background:#22223a;border:1px solid #444;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.4);z-index:10;overflow:hidden">
<div style="padding:8px 12px;background:#3a3a5a;display:flex;align-items:center;gap:8px">
<span style="opacity:0.4">&#128269;</span> <span>happy birthday</span>
</div>
<div style="padding:8px 12px;display:flex;align-items:center;gap:8px">
<span style="opacity:0.4">&#128269;</span> <span>happy</span>
</div>
<div style="padding:8px 12px;display:flex;align-items:center;gap:8px">
<span style="opacity:0.4">&#128269;</span> <span>happy easter</span>
</div>
<div style="padding:8px 12px;display:flex;align-items:center;gap:8px">
<span style="opacity:0.4">&#128269;</span> <span>happy dance</span>
</div>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:12px;filter:blur(1px);opacity:0.5">
<div style="flex:1;display:flex;flex-direction:column;gap:8px">
<div style="background:#333;border-radius:8px;height:90px"></div>
<div style="background:#333;border-radius:8px;height:70px"></div>
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:8px">
<div style="background:#333;border-radius:8px;height:70px"></div>
<div style="background:#333;border-radius:8px;height:90px"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section" style="margin-top:24px">
<h3>My recommendation: Option A (inline list)</h3>
<div class="pros-cons">
<div class="pros">
<h4>Why A</h4>
<ul>
<li>No Portal/z-index issues inside the bottom sheet (native) or modal (web)</li>
<li>Naturally fits the reserved slot in GifPickerHeader ("future: tabs render here")</li>
<li>Clear visual hierarchy — suggestions push the grid down, making them prominent</li>
<li>Simple to implement — just a conditional list render</li>
</ul>
</div>
<div class="cons">
<h4>Tradeoffs</h4>
<ul>
<li>Takes more vertical space than chips (B) when visible</li>
<li>Doesn't reuse Sift (C), but that's OK — Sift solves cursor-anchored popovers which we don't need here</li>
</ul>
</div>
</div>
</div>
@@ -0,0 +1,3 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>
@@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1776191006962}
@@ -0,0 +1,5 @@
{"type":"server-started","port":49342,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:49342","screen_dir":"/Users/pope/dev/bsky/social-app/.superpowers/brainstorm/86663-1776187526/content","state_dir":"/Users/pope/dev/bsky/social-app/.superpowers/brainstorm/86663-1776187526/state"}
{"type":"screen-added","file":"/Users/pope/dev/bsky/social-app/.superpowers/brainstorm/86663-1776187526/content/suggestion-placement.html"}
{"type":"screen-added","file":"/Users/pope/dev/bsky/social-app/.superpowers/brainstorm/86663-1776187526/content/waiting.html"}
{"type":"screen-added","file":"/Users/pope/dev/bsky/social-app/.superpowers/brainstorm/86663-1776187526/content/design-overview.html"}
{"type":"server-stopped","reason":"idle timeout"}
@@ -0,0 +1 @@
86672