@magicx-eng/ai-autocomplete-vanilla 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +291 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# @magicx-eng/ai-autocomplete-vanilla
|
|
2
|
+
|
|
3
|
+
A framework-agnostic vanilla JS/TypeScript library that provides a guided AI-powered autocomplete experience with pill-based input and dropdown suggestions. Works with any tech stack — React, Vue, Svelte, Angular, or plain HTML.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Zero dependencies** — no React, no framework, no virtual DOM
|
|
8
|
+
- **Pill-based input** — inline pills for unfilled parameters, completed text for filled ones
|
|
9
|
+
- **Pill placement** — render pills inline in the input or inside the dropdown
|
|
10
|
+
- **Light/dark mode** — built-in themes with `prefers-color-scheme` support, fully overridable via CSS variables
|
|
11
|
+
- **Access token auth** — short-lived tokens with automatic refresh, single-flight deduplication, and 401 retry
|
|
12
|
+
- **Keyboard navigation** — arrow keys, enter to submit, tab to autocomplete
|
|
13
|
+
- **Client-side filtering** — instant substring filtering on every keystroke
|
|
14
|
+
- **Option overrides** — inject or dynamically generate client-side options per suggestion type
|
|
15
|
+
- **Controlled & uncontrolled** — works out of the box or integrates with external state via `setValue()` / `setCompletedParams()`
|
|
16
|
+
- **Imperative API** — `focus()`, `reset()`, `setMode()`, `destroy()`, and more
|
|
17
|
+
- **Accessible** — ARIA combobox pattern with `role="listbox"`, `aria-activedescendant`
|
|
18
|
+
- **Animations** — option selection streak animation, text shimmer on newly added params
|
|
19
|
+
- **Lightweight** — ~10 KB gzipped, styles auto-injected at runtime
|
|
20
|
+
- **TypeScript first** — full type definitions shipped with the package
|
|
21
|
+
- **SSR-safe** — no top-level `document`/`window` access
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm add @magicx-eng/ai-autocomplete-vanilla
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<div id="ac"></div>
|
|
33
|
+
|
|
34
|
+
<script type="module">
|
|
35
|
+
import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
|
|
36
|
+
|
|
37
|
+
const instance = new AIAutocomplete(document.getElementById("ac"), {
|
|
38
|
+
apiConfig: {
|
|
39
|
+
endpoint: "https://api.example.com/ac/suggest",
|
|
40
|
+
apiKey: "your_api_key",
|
|
41
|
+
},
|
|
42
|
+
onSubmit: (result) => {
|
|
43
|
+
console.log(result.query); // "Create a email"
|
|
44
|
+
console.log(result.raw_query); // "Create a {{TASK_1}}"
|
|
45
|
+
console.log(result.completed_params); // [{ placeholder: "{{TASK_1}}", type: "task", ... }]
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
No CSS import needed — styles are auto-injected on first instantiation.
|
|
52
|
+
|
|
53
|
+
## Usage with Frameworks
|
|
54
|
+
|
|
55
|
+
### Vue
|
|
56
|
+
|
|
57
|
+
```vue
|
|
58
|
+
<template>
|
|
59
|
+
<div ref="acRef" />
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script setup>
|
|
63
|
+
import { ref, onMounted, onUnmounted } from "vue";
|
|
64
|
+
import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
|
|
65
|
+
|
|
66
|
+
const acRef = ref(null);
|
|
67
|
+
let instance;
|
|
68
|
+
|
|
69
|
+
onMounted(() => {
|
|
70
|
+
instance = new AIAutocomplete(acRef.value, {
|
|
71
|
+
apiConfig: { endpoint: "/ac/suggest", apiKey: "your_key" },
|
|
72
|
+
onSubmit: (result) => console.log(result),
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
onUnmounted(() => instance?.destroy());
|
|
77
|
+
</script>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Svelte
|
|
81
|
+
|
|
82
|
+
```svelte
|
|
83
|
+
<script>
|
|
84
|
+
import { onMount, onDestroy } from "svelte";
|
|
85
|
+
import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
|
|
86
|
+
|
|
87
|
+
let el;
|
|
88
|
+
let instance;
|
|
89
|
+
|
|
90
|
+
onMount(() => {
|
|
91
|
+
instance = new AIAutocomplete(el, {
|
|
92
|
+
apiConfig: { endpoint: "/ac/suggest", apiKey: "your_key" },
|
|
93
|
+
onSubmit: (result) => console.log(result),
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
onDestroy(() => instance?.destroy());
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<div bind:this={el}></div>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Plain HTML (CDN)
|
|
104
|
+
|
|
105
|
+
```html
|
|
106
|
+
<div id="ac"></div>
|
|
107
|
+
<script type="module">
|
|
108
|
+
import { AIAutocomplete } from "https://unpkg.com/@magicx-eng/ai-autocomplete-vanilla/index.mjs";
|
|
109
|
+
|
|
110
|
+
new AIAutocomplete(document.getElementById("ac"), {
|
|
111
|
+
apiConfig: { endpoint: "/ac/suggest", apiKey: "your_key" },
|
|
112
|
+
onSubmit: (result) => console.log(result),
|
|
113
|
+
});
|
|
114
|
+
</script>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## API Reference
|
|
118
|
+
|
|
119
|
+
### Constructor
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
new AIAutocomplete(container: HTMLElement, options?: CoreOptions)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `CoreOptions`
|
|
126
|
+
|
|
127
|
+
| Option | Type | Default | Description |
|
|
128
|
+
|---|---|---|---|
|
|
129
|
+
| `apiConfig` | `APIConfig` | — | API configuration (see below). |
|
|
130
|
+
| `optionOverrides?` | `Record<string, (query: string) => SuggestionOption[]>` | — | Override options per suggestion type. |
|
|
131
|
+
| `placeholder?` | `string` | — | Fallback placeholder when the server doesn't return one. |
|
|
132
|
+
| `columns?` | `number` | `2` | Number of columns in the dropdown grid. |
|
|
133
|
+
| `typewriterEffect?` | `boolean` | `false` | Letter-by-letter reveal on option select. |
|
|
134
|
+
| `pillPlacement?` | `"inline" \| "dropdown"` | `"inline"` | Where to render unfilled pills. |
|
|
135
|
+
| `mode?` | `"light" \| "dark" \| "auto"` | `"auto"` | Color mode. `"auto"` follows `prefers-color-scheme`. |
|
|
136
|
+
| `optionsPosition?` | `"above" \| "below"` | `"below"` | Where the dropdown opens relative to the input. |
|
|
137
|
+
| `animations?` | `boolean` | `true` | Enable/disable all animations (streak + shimmer). |
|
|
138
|
+
| `onSubmit?` | `(result: AutocompleteResult) => void` | — | Called on Enter or submit button. |
|
|
139
|
+
| `onError?` | `(error: Error) => void` | — | Called when a fetch fails. |
|
|
140
|
+
| `onChange?` | `(text: string) => void` | — | Called when text changes. |
|
|
141
|
+
| `onParamsChange?` | `(params: CompletedParamState[]) => void` | — | Called when completed params change. |
|
|
142
|
+
| `value?` | `string` | — | Controlled text value. |
|
|
143
|
+
| `completedParams?` | `CompletedParamState[]` | — | Controlled completed params. |
|
|
144
|
+
|
|
145
|
+
### `APIConfig`
|
|
146
|
+
|
|
147
|
+
A discriminated union: `APIKeyConfig | AccessTokenConfig`.
|
|
148
|
+
|
|
149
|
+
#### API Key Mode (default)
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
{
|
|
153
|
+
apiKey: "your_api_key",
|
|
154
|
+
authScheme: "Bearer", // or "Basic"
|
|
155
|
+
endpoint: "/ac/suggest", // optional
|
|
156
|
+
appIdentifier: "my-app", // optional
|
|
157
|
+
headers: { "X-Custom": "v" }, // optional
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Access Token Mode
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
{
|
|
165
|
+
type: "accessToken",
|
|
166
|
+
getAccessToken: async () => {
|
|
167
|
+
const res = await fetch("/api/ac-token");
|
|
168
|
+
const { access_token, expires_at } = await res.json();
|
|
169
|
+
return { accessToken: access_token, expiresAt: expires_at };
|
|
170
|
+
},
|
|
171
|
+
endpoint: "/ac/suggest", // optional
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The SDK handles token refresh transparently: if a request returns 401, it calls `getAccessToken` once, retries, and only surfaces an error if the retry also fails. Concurrent 401s share a single refresh call.
|
|
176
|
+
|
|
177
|
+
### Instance Methods
|
|
178
|
+
|
|
179
|
+
| Method | Description |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `focus()` | Focus the textarea. |
|
|
182
|
+
| `reset()` | Clear all state and re-fetch initial suggestions. |
|
|
183
|
+
| `destroy()` | Remove all DOM, listeners, and timers. Call when unmounting. |
|
|
184
|
+
| `setMode(mode)` | Switch color mode at runtime (`"light"`, `"dark"`, `"auto"`). |
|
|
185
|
+
| `update(opts)` | Update any option at runtime (mode, pillPlacement, animations, etc.). |
|
|
186
|
+
| `setValue(text)` | Set text value (controlled mode). |
|
|
187
|
+
| `setCompletedParams(params)` | Set completed params (controlled mode). |
|
|
188
|
+
| `setActivePill(index)` | Reorder pills, making the one at `index` active. |
|
|
189
|
+
| `removeLastParam()` | Remove the last completed param and restore it as a pill. |
|
|
190
|
+
| `clearNewParamId()` | Clear the shimmer animation state. |
|
|
191
|
+
| `getState()` | Get a snapshot of the full internal state. |
|
|
192
|
+
| `isReady` | `true` when the server indicates the query is ready for execution. |
|
|
193
|
+
|
|
194
|
+
### Event Subscription
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const off = instance.on("submit", (result) => { ... });
|
|
198
|
+
off(); // unsubscribe
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Events: `submit`, `error`, `change`, `paramsChange`, `stateChange`.
|
|
202
|
+
|
|
203
|
+
Constructor callbacks (`onSubmit`, `onChange`, etc.) are equivalent to calling `on()`.
|
|
204
|
+
|
|
205
|
+
### `AutocompleteResult`
|
|
206
|
+
|
|
207
|
+
The object passed to `onSubmit`:
|
|
208
|
+
|
|
209
|
+
| Field | Type | Description |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `query` | `string` | Plain text as the user sees it. |
|
|
212
|
+
| `raw_query` | `string` | Text with placeholder tokens (e.g. `"Create a {{TASK_1}}"`). |
|
|
213
|
+
| `completed_params` | `CompletedParam[]` | Array of filled parameter values. |
|
|
214
|
+
|
|
215
|
+
## CSS Customization
|
|
216
|
+
|
|
217
|
+
Styles are auto-injected at runtime — no CSS import needed. The component ships built-in light and dark defaults that apply automatically based on the `mode` option.
|
|
218
|
+
|
|
219
|
+
### CSS Variables
|
|
220
|
+
|
|
221
|
+
Override these on the container element to customize the appearance. All built-in defaults use `:where()` (zero specificity), so your overrides always win without `!important`.
|
|
222
|
+
|
|
223
|
+
| Variable | Default (light) | Default (dark) | Description |
|
|
224
|
+
|---|---|---|---|
|
|
225
|
+
| `--ac-pill-bg` | `#f1f5f9` | `#1e293b` | Pill background color |
|
|
226
|
+
| `--ac-pill-color` | `#475569` | `#cbd5e1` | Pill text color |
|
|
227
|
+
| `--ac-pill-font-size` | `19px` | `19px` | Pill font size |
|
|
228
|
+
| `--ac-option-bg` | `transparent` | `transparent` | Option background (highlighted) |
|
|
229
|
+
| `--ac-option-color` | `#475569` | `#cbd5e1` | Option text color |
|
|
230
|
+
| `--ac-option-color-selected` | `#0f172a` | `#f8fafc` | Highlighted option text color |
|
|
231
|
+
| `--ac-option-font-size` | `19px` | `19px` | Option font size |
|
|
232
|
+
| `--ac-written-text-color` | `#0f172a` | `#f1f5f9` | Input text color |
|
|
233
|
+
| `--ac-written-text-font-size` | `19px` | `19px` | Input text font size |
|
|
234
|
+
|
|
235
|
+
### Per-mode Overrides
|
|
236
|
+
|
|
237
|
+
```css
|
|
238
|
+
#my-autocomplete[data-mode="light"] {
|
|
239
|
+
--ac-pill-bg: #e2e8f0;
|
|
240
|
+
--ac-written-text-color: #1e293b;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#my-autocomplete[data-mode="dark"] {
|
|
244
|
+
--ac-pill-bg: #334155;
|
|
245
|
+
--ac-written-text-color: #f8fafc;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Live Preview
|
|
250
|
+
|
|
251
|
+
CSS variables update instantly — great for theme editors:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
document.getElementById("my-autocomplete")
|
|
255
|
+
.style.setProperty("--ac-pill-bg", newColor);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Keyboard Behavior
|
|
259
|
+
|
|
260
|
+
| Key | Behavior |
|
|
261
|
+
|---|---|
|
|
262
|
+
| **Arrow Down** | Open dropdown and select first option, or navigate to next option. |
|
|
263
|
+
| **Arrow Up** | Navigate to previous option. Deselects if on the first row. |
|
|
264
|
+
| **Arrow Right** | Jump to next column when an option is highlighted. Cycles to next pill when cursor is at the end. |
|
|
265
|
+
| **Arrow Left** | Jump to previous column when an option is highlighted. |
|
|
266
|
+
| **Tab** | Select the first tappable option (or highlighted one). Accepts placeholder when input is empty. |
|
|
267
|
+
| **Enter** | Select highlighted option if one is highlighted. Otherwise submits the query. |
|
|
268
|
+
| **Escape** | Deselect the highlighted option. |
|
|
269
|
+
|
|
270
|
+
## Option Overrides
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
new AIAutocomplete(el, {
|
|
274
|
+
optionOverrides: {
|
|
275
|
+
account: () => [
|
|
276
|
+
{ text: "Savings", is_tappable: true, kind: null },
|
|
277
|
+
{ text: "Checking", is_tappable: true, kind: null },
|
|
278
|
+
],
|
|
279
|
+
value: (query) => {
|
|
280
|
+
const digits = query.replace(/\D/g, "");
|
|
281
|
+
if (!digits) return [{ text: "$100", is_tappable: true, kind: null }];
|
|
282
|
+
return [{ text: `$${digits}`, is_tappable: true, kind: null }];
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
onSubmit: handleSubmit,
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
Private package. All rights reserved.
|