@extedcoud/smart-components
v0.0.1
Published
React UI component library
Downloads
13
Readme
@extedcoud/smart-components
Headless React components with the AI wiring already done. You bring an LLM client and your own styles; the library handles the fiddly parts — debouncing, aborting stale requests, ghost-text positioning, keyboard handling.
📖 Docs + live demos · 🎮 Storybook
Install
pnpm add @extedcoud/smart-components react react-domNo runtime dependencies beyond React. Every adapter is built on plain fetch.
Quick start
import { SmartProvider, SmartTextbox, SmartSuggestion } from '@extedcoud/smart-components';
import { createProxyClient } from '@extedcoud/smart-components/adapters/proxy';
import '@extedcoud/smart-components/style.css';
// In production, point this at your own backend so the API key stays server-side.
const client = createProxyClient({ url: '/api/smart' });
export function App() {
const [v, setV] = useState('');
return (
<SmartProvider client={client} model="gpt-4o-mini">
<SmartTextbox value={v} onChange={setV} context="user is writing a support reply" />
<SmartSuggestion
value={v}
onChange={setV}
onSelect={(s) => console.log('picked', s)}
context="naming a new project"
/>
</SmartProvider>
);
}Components
| Component | What it does |
| --- | --- |
| <SmartTextbox> | Single-line input with Copilot-style ghost completion. ArrowRight accepts (configurable), Esc dismisses. |
| <SmartTextarea> | Multiline ghost completion via a mirror div, with multiline stops and optional auto-resize. |
| <SmartSuggestion> | Combobox with an AI-generated dropdown. Arrow keys to move, Enter to pick. |
| <SmartRewrite> | Render-prop rewrite primitive. Ships with Shorter / Formal / Casual / Fix grammar presets. |
Everything is headless: minimal default DOM, render slots (renderItem, renderGhost, render-props), and native input attributes pass straight through.
Styling the ghost text
The ghost defaults to opacity: 0.4 and inherits the input's color. To recolor it, pass ghostClassName or ghostStyle:
<SmartTextbox value={v} onChange={setV} ghostStyle={{ color: '#0066cc', fontStyle: 'italic' }} />For richer markup (icons, badges) use the renderGhost render-prop. To style globally, target [data-testid="smart-textbox-ghost"] / [data-testid="smart-textarea-ghost"].
Mobile
Touch is a first-class target, not an afterthought. Things to know:
- The accept key is configurable. It defaults to
ArrowRight, which most soft keyboards lack — on mobile, remapacceptKeyor trigger accept imperatively via the field ref. - Wrappers use
touch-action: manipulationto drop the 300ms tap delay. SmartSuggestionselection runs on pointer events (mouse, touch, pen). Give itemsmin-height: 44pxto hit the WCAG touch target.- Inputs below
16pxfont size make iOS Safari zoom on focus — keepSmartTextbox/SmartTextareaat 16px or larger. - Known limit: the
SmartSuggestiondropdown can sit under the soft keyboard on short viewports. Portal the list or use a fullscreen picker there.
AI clients
SmartClient is a capability-based interface. Use an adapter or implement your own:
| Adapter | Import | When |
| --- | --- | --- |
| createProxyClient | /adapters/proxy | Production. POSTs to your backend; the key stays on the server. |
| createOpenAIClient | /adapters/openai | Dev and demos only — never ship a key to the browser. |
| createAnthropicClient | /adapters/anthropic | Anthropic Messages API (complete + stream). |
| createMockClient | /adapters/mock | Tests and Storybook. |
Rolling your own is a few lines:
import { SMART_CLIENT_PROTOCOL_VERSION, type SmartClient } from '@extedcoud/smart-components';
const myClient: SmartClient = {
protocolVersion: SMART_CLIENT_PROTOCOL_VERSION,
id: 'my-backend',
capabilities: new Set(['complete', 'stream']),
async complete(req) { /* ... */ return text; },
async *stream(req) { /* yield chunks */ },
};Hooks
The components are thin wrappers over hooks. Reach for these to build your own on the same plumbing:
import {
useGhostCompletion,
useSuggestionList,
useRewrite,
useSmartState,
} from '@extedcoud/smart-components';useSmartState — useState that an LLM can fill
A drop-in useState with one extra: ai.generate(context?). The shape is read from your initial value, so the model is constrained to matching JSON — no schema, no parsing.
const [user, setUser, ai] = useSmartState(
{ name: '', age: 0, bio: '' },
'a fictitious cyberpunk character',
);
ai.generate(); // → { name: 'Kael-9', age: 28, bio: '…' }
setUser({ ... }); // still plain useStateWhen the seed is empty (null, []), pass options.shape so it knows what to ask for:
const [tags, , ai] = useSmartState<string[]>([], 'tags for a sci-fi blog post', {
shape: { type: 'array', item: 'string' },
});A few things worth knowing:
- Calling
setValuemid-generate cancels the in-flight request — the user's edit wins. - An empty inner array (
{ tags: [] }) throws at mount; seed it or passshape. Same forDate,Map,Set, functions — JSON-serializable values only. - Results are cached LRU(16) by
(shape, context). Opt out with{ cache: false }. - Generate is atomic (no streaming) and needs a client with the
completecapability.
Full walkthrough — form autofill, text extraction, palettes — is in the docs.
Develop
A pnpm workspace: the root is the library, docs/ is the public site (Next.js + Fumadocs on GitHub Pages).
pnpm install
pnpm storybook # :6006 — internal dev surface
pnpm test # vitest watch
pnpm lint
pnpm typecheck
pnpm build # lib → dist/
# Docs site (build the lib first — it consumes dist/)
pnpm --filter @extedcoud/smart-components build
pnpm --filter docs dev # :3000Docs deploy to GitHub Pages via .github/workflows/deploy-docs.yml on push to main.
Stack
Library: Vite (lib mode), TypeScript, CSS Modules, ESLint 9, Prettier, Vitest + RTL, Storybook. Docs: Next.js 15 (static export), Fumadocs UI, Tailwind v4, MDX.
