smart-components
Components

SmartSuggestion

Combobox with AI-generated dropdown suggestions, keyboard nav, and full ARIA.

An input + listbox combobox that streams suggestions from your SmartClient. Arrow keys to navigate, Enter to pick, Esc to close. Implements the ARIA 1.2 combobox pattern.

Preview

    import { useState } from 'react';import { SmartProvider, SmartSuggestion } from '@extedcoud/smart-components';import { createMockClient } from '@extedcoud/smart-components/adapters/mock';const client = createMockClient({complete: async () => ['Apple', 'Apricot', 'Banana', 'Blueberry'].join('\n'),});export function Example() {const [value, setValue] = useState('');return (  <SmartProvider client={client}>    <SmartSuggestion      value={value}      onChange={setValue}      onSelect={(item) => console.log('picked', item)}      placeholder="Type to filter…"    />  </SmartProvider>);}

    Styled example — command palette

    A ⌘K-style command palette with icons, kbd hints, gradient selection, and a status footer.

      ↑↓ to navigate · ↵ to select · esc to dismiss10 actions
      'use client';import { useState } from 'react';import { SmartProvider, SmartSuggestion } from '@extedcoud/smart-components';import { makeSuggestionMock } from '@/lib/mock-client';const COMMANDS = [  'Open recent file',  'Open settings',  'Run build',  'Run tests',  'Toggle theme',  'Create new component',  'Create new hook',  'Search in workspace',  'Show keyboard shortcuts',  'Sign out',];const ICONS: Record<string, string> = {  Open: '📂',  Run: '⚡',  Toggle: '🌓',  Create: '✨',  Search: '🔍',  Show: '⌨️',  Sign: '🚪',};function iconFor(label: string) {  const first = label.split(' ')[0];  return ICONS[first] ?? '·';}const client = makeSuggestionMock(COMMANDS, 180);export default function SmartSuggestionStylized() {  const [value, setValue] = useState('');  return (    <SmartProvider client={client}>      <div className="w-full max-w-md">        <div className="overflow-hidden rounded-xl border border-fd-border bg-fd-card shadow-xl shadow-fd-foreground/5">          <div className="flex items-center gap-2 border-b border-fd-border px-3 py-2 text-fd-muted-foreground">            <span aria-hidden>⌘</span>            <SmartSuggestion              value={value}              onChange={setValue}              placeholder="Type a command…"              wrapperClassName="relative flex-1"              className="w-full bg-transparent text-base text-fd-foreground outline-none placeholder:text-fd-muted-foreground"              listClassName="absolute left-0 right-0 top-full z-10 mt-2 max-h-72 overflow-y-auto rounded-lg border border-fd-border bg-fd-card p-1 shadow-xl"              itemClassName="flex items-center gap-3 rounded-md px-3 py-2 text-sm text-fd-foreground transition aria-selected:bg-gradient-to-r aria-selected:from-indigo-500/15 aria-selected:to-fuchsia-500/15 aria-selected:text-fd-foreground hover:bg-fd-accent cursor-pointer"              renderItem={(item, { active }) => (                <>                  <span className="text-lg" aria-hidden>                    {iconFor(item)}                  </span>                  <span className="flex-1">{item}</span>                  {active && (                    <kbd className="rounded border border-fd-border bg-fd-background px-1.5 py-0.5 text-[10px] font-mono text-fd-muted-foreground">                    </kbd>                  )}                </>              )}            />            <kbd className="hidden rounded border border-fd-border bg-fd-background px-1.5 py-0.5 text-[10px] font-mono text-fd-muted-foreground sm:block">              ⌘ K            </kbd>          </div>          <div className="flex items-center justify-between bg-fd-secondary/30 px-3 py-1.5 text-[11px] text-fd-muted-foreground">            <span>↑↓ to navigate · ↵ to select · esc to dismiss</span>            <span>{COMMANDS.length} actions</span>          </div>        </div>      </div>    </SmartProvider>  );}

      Custom item renderer

      <SmartSuggestion
        value={v}
        onChange={setV}
        renderItem={(item, { active }) => (
          <span style={{ fontWeight: active ? 700 : 400 }}>
            <SparkleIcon /> {item}
          </span>
        )}
      />

      Empty + loading states

      <SmartSuggestion
        value={v}
        onChange={setV}
        renderEmpty={() => <em>No matches</em>}
        renderLoading={() => <span>Thinking…</span>}
      />

      Props

      PropTypeDefaultDescription
      value*stringControlled input value.
      onChange*(value: string) => voidCalled on user input.
      onSelect(item: string) => voidCalled when the user selects a suggestion (Enter, click, or keyboard).
      contextstringOptional context string included in the prompt.
      countnumber5Max suggestions requested.
      debounceMsnumber300Debounce window in ms.
      minCharsnumber1Minimum chars before suggestions fetch.
      disableAIbooleanfalseDisable AI calls.
      renderItem(item, { active, index }) => ReactNodeRender override for each item.
      renderEmpty() => ReactNodeRender override when results are empty.
      renderLoading() => ReactNodeRender override while loading.
      wrapperClassNamestringClass name on the outer wrapper.
      listClassNamestringClass name on the listbox.
      itemClassNamestringClass name on each item.
      ...restInputHTMLAttributes<HTMLInputElement>All native input props (combobox aria-* and onSelect are managed internally).

      Mobile notes

      • Pointer events are used for selection — touch + mouse + pen unified.
      • Render items with min-height: 44px to meet WCAG touch-target minimum.
      • Known limitation: soft-keyboard overlap on narrow viewports. For phone-sized layouts, hoist the list via a portal or render a fullscreen picker.