@magicx-eng/ai-autocomplete-vanilla 0.1.1 → 0.1.3

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 CHANGED
@@ -4,6 +4,7 @@ A framework-agnostic vanilla JS/TypeScript library that provides a guided AI-pow
4
4
 
5
5
  ## Features
6
6
 
7
+ - **Three tiers of integration** — full drop-in component, dropdown-only, or fully headless
7
8
  - **Zero dependencies** — no React, no framework, no virtual DOM
8
9
  - **Pill-based input** — inline pills for unfilled parameters, completed text for filled ones
9
10
  - **Pill placement** — render pills inline in the input or inside the dropdown
@@ -12,8 +13,7 @@ A framework-agnostic vanilla JS/TypeScript library that provides a guided AI-pow
12
13
  - **Keyboard navigation** — arrow keys, enter to submit, tab to autocomplete
13
14
  - **Client-side filtering** — instant substring filtering on every keystroke
14
15
  - **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
16
+ - **Controlled & uncontrolled** — works out of the box or integrates with external state
17
17
  - **Accessible** — ARIA combobox pattern with `role="listbox"`, `aria-activedescendant`
18
18
  - **Animations** — option selection streak animation, text shimmer on newly added params
19
19
  - **Lightweight** — ~10 KB gzipped, styles auto-injected at runtime
@@ -26,121 +26,219 @@ A framework-agnostic vanilla JS/TypeScript library that provides a guided AI-pow
26
26
  pnpm add @magicx-eng/ai-autocomplete-vanilla
27
27
  ```
28
28
 
29
- ## Quick Start
29
+ ## Three Tiers
30
30
 
31
- ```html
32
- <div id="ac"></div>
31
+ | Tier | What you get | What you own | Use when |
32
+ |---|---|---|---|
33
+ | **Tier 1: Full** | Input + dropdown + pills + keyboard + state | Nothing — drop in and go | You want a complete widget with zero setup |
34
+ | **Tier 2: Dropdown** | Dropdown + pills + keyboard + state | The input element | You need a custom input (e.g. rich text, search bar) |
35
+ | **Tier 3: Headless** | State + controllers + derived data | All DOM | You're building a framework wrapper (React, Vue, Svelte) or need full rendering control |
33
36
 
34
- <script type="module">
35
- import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
37
+ ---
36
38
 
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>
39
+ ## Tier 1: Full Component
40
+
41
+ Drop-in. The library creates and owns the input, dropdown, pills, and all DOM.
42
+
43
+ ```ts
44
+ import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
45
+
46
+ const ac = new AIAutocomplete(document.getElementById("root"), {
47
+ apiConfig: {
48
+ endpoint: "https://api.example.com/ac/suggest",
49
+ apiKey: "your_api_key",
50
+ },
51
+ onSubmit: (result) => {
52
+ console.log(result.query); // "Create a email"
53
+ console.log(result.raw_query); // "Create a {{TASK_1}}"
54
+ console.log(result.completed_params); // [{ placeholder: "{{TASK_1}}", type: "task", ... }]
55
+ },
56
+ });
49
57
  ```
50
58
 
51
59
  No CSS import needed — styles are auto-injected on first instantiation.
52
60
 
53
- ## Usage with Frameworks
61
+ ### Options
62
+
63
+ ```ts
64
+ const ac = new AIAutocomplete(container, {
65
+ // Tier: "full" (default) | "dropdown" | "headless"
66
+ renderMode: "full",
67
+
68
+ // API
69
+ apiConfig: { apiKey: "...", authScheme: "Bearer", endpoint: "/ac/suggest" },
70
+ optionOverrides: { account: (query) => [...] },
71
+ columns: 2,
72
+ maskCompletedText: false, // when true, omits completed params' text from API requests (PII masking)
73
+
74
+ // Appearance
75
+ mode: "auto", // "light" | "dark" | "auto"
76
+ optionsPosition: "below", // "above" | "below"
77
+ animations: true, // enable streak + shimmer
78
+ pillPlacement: "inline", // "inline" | "dropdown" | "hidden"
79
+ dropdownTrigger: "auto", // "auto" | "manual" | "hidden"
80
+ closeDropdownOnBlur: true, // false = keep dropdown open even when input loses focus
81
+
82
+ // Focus
83
+ autoFocus: true, // focus the input on mount (Tier 1 only)
84
+
85
+ // Events
86
+ onSubmit: (result) => { ... },
87
+ onError: (error) => { ... },
88
+ onChange: (text) => { ... },
89
+ onParamsChange: (params) => { ... },
90
+ onFocus: () => { ... },
91
+ onBlur: () => { ... },
92
+
93
+ // Controlled mode
94
+ value: "initial text",
95
+ completedParams: [],
96
+ });
97
+ ```
98
+
99
+ ### Methods
100
+
101
+ ```ts
102
+ ac.focus(); // Focus the input
103
+ ac.blur(); // Blur the input
104
+ ac.reset(); // Clear everything, re-fetch
105
+ ac.destroy(); // Remove DOM, listeners, timers
106
+ ac.setMode("dark"); // Switch color mode
107
+ ac.update({ animations: false, optionsPosition: "above" });
108
+ ```
109
+
110
+ > `autoFocus`, `focus()`, and `blur()` only operate on the textarea the library owns (Tier 1 / `renderMode: "full"`). In Tier 2/3, the consumer owns the input and calls `.focus()` / `.blur()` on their own element; the `onFocus` / `onBlur` callbacks still fire whenever `setFocused()` is called.
111
+
112
+ ---
54
113
 
55
- ### Vue
114
+ ## Tier 2: Dropdown Only
56
115
 
57
- ```vue
58
- <template>
59
- <div ref="acRef" />
60
- </template>
116
+ You own the input. The library renders the dropdown with pills inside a container you provide.
61
117
 
62
- <script setup>
63
- import { ref, onMounted, onUnmounted } from "vue";
118
+ ```ts
64
119
  import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
65
120
 
66
- const acRef = ref(null);
67
- let instance;
121
+ const input = document.getElementById("my-input");
122
+ const dropdownContainer = document.getElementById("my-dropdown");
68
123
 
69
- onMounted(() => {
70
- instance = new AIAutocomplete(acRef.value, {
71
- apiConfig: { endpoint: "/ac/suggest", apiKey: "your_key" },
72
- onSubmit: (result) => console.log(result),
73
- });
124
+ const ac = new AIAutocomplete(dropdownContainer, {
125
+ renderMode: "dropdown",
126
+ apiConfig: { apiKey: "your_api_key" },
127
+ onChange: (text) => {
128
+ // Keep your input in sync when the library updates text (e.g. option selected)
129
+ if (input.value !== text) input.value = text;
130
+ },
131
+ onSubmit: (result) => console.log(result),
74
132
  });
75
133
 
76
- onUnmounted(() => instance?.destroy());
77
- </script>
134
+ // Wire your input → library
135
+ input.addEventListener("input", () => ac.handleTextChange(input.value));
136
+ input.addEventListener("keydown", (e) => ac.handleKeyDown(e));
137
+ input.addEventListener("focus", () => ac.setFocused(true));
138
+ input.addEventListener("blur", () => ac.setFocused(false));
78
139
  ```
79
140
 
80
- ### Svelte
141
+ Pills always render inside the dropdown in this mode. The library creates the dropdown DOM inside your container — you just provide the element and wire up your input's events.
81
142
 
82
- ```svelte
83
- <script>
84
- import { onMount, onDestroy } from "svelte";
85
- import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
143
+ > Focus/blur wiring is required when `dropdownTrigger` is `"auto"` (the default) — the dropdown only opens while the input is focused. Without `setFocused()`, the dropdown will never open.
86
144
 
87
- let el;
88
- let instance;
145
+ ---
89
146
 
90
- onMount(() => {
91
- instance = new AIAutocomplete(el, {
92
- apiConfig: { endpoint: "/ac/suggest", apiKey: "your_key" },
93
- onSubmit: (result) => console.log(result),
94
- });
95
- });
147
+ ## Tier 3: Headless
96
148
 
97
- onDestroy(() => instance?.destroy());
98
- </script>
149
+ No DOM at all. You get state, controllers, and derived data. You render everything yourself.
99
150
 
100
- <div bind:this={el}></div>
101
- ```
151
+ This is what framework wrappers (React, Vue, Svelte) use under the hood.
152
+
153
+ ```ts
154
+ import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
102
155
 
103
- ### Plain HTML (CDN)
156
+ const ac = new AIAutocomplete(document.createElement("div"), {
157
+ renderMode: "headless",
158
+ apiConfig: { apiKey: "your_api_key" },
159
+ onSubmit: (result) => console.log(result),
160
+ });
104
161
 
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";
162
+ // Read state
163
+ const state = ac.getState();
164
+ // state.text, state.completedParams, state.suggestions,
165
+ // state.segments, state.filteredOptions, state.actionableSuggestions,
166
+ // state.placeholderText, state.isDropdownOpen, state.activeDropdownIndex,
167
+ // state.newParamId, state.isLoading, state.isReady, state.error
168
+
169
+ // Subscribe to changes (fires after derived state is settled)
170
+ const unsub = ac.subscribe((state) => {
171
+ renderMyInput(state.text, state.placeholderText);
172
+ renderMyPills(state.actionableSuggestions);
173
+ renderMyDropdown(state.filteredOptions, state.activeDropdownIndex, state.isDropdownOpen);
174
+ renderMySegments(state.segments, state.newParamId);
175
+ });
109
176
 
110
- new AIAutocomplete(document.getElementById("ac"), {
111
- apiConfig: { endpoint: "/ac/suggest", apiKey: "your_key" },
112
- onSubmit: (result) => console.log(result),
113
- });
114
- </script>
177
+ // Forward user actions → library
178
+ myInput.addEventListener("input", () => ac.handleTextChange(myInput.value));
179
+ myInput.addEventListener("keydown", (e) => ac.handleKeyDown(e));
180
+ myInput.addEventListener("focus", () => ac.setFocused(true));
181
+ myInput.addEventListener("blur", () => ac.setFocused(false));
182
+ myOptionEl.addEventListener("click", () => ac.selectOption(option));
183
+ myPillEl.addEventListener("click", () => ac.setActivePill(index));
184
+
185
+ // Cleanup
186
+ ac.destroy();
187
+ unsub();
115
188
  ```
116
189
 
117
- ## API Reference
190
+ ### State shape (`CoreState`)
118
191
 
119
- ### Constructor
192
+ | Field | Type | Description |
193
+ |---|---|---|
194
+ | `text` | `string` | Current input text |
195
+ | `completedParams` | `CompletedParamState[]` | Filled parameters |
196
+ | `suggestions` | `Suggestion[]` | All suggestions from server (including placeholder type) |
197
+ | `actionableSuggestions` | `Suggestion[]` | Non-placeholder suggestions (the pills) |
198
+ | `filteredOptions` | `SuggestionOption[]` | Options for the active suggestion, filtered by current query |
199
+ | `segments` | `Segment[]` | Input text split into typed text vs completed params — for overlay rendering |
200
+ | `placeholderText` | `string` | Placeholder text from server suggestions (joined `placeholder`-type suggestion texts) |
201
+ | `activeDropdownIndex` | `number` | Highlighted option index (-1 = none) |
202
+ | `isDropdownOpen` | `boolean` | Whether the dropdown should be visible |
203
+ | `newParamId` | `string \| null` | ID of the most recently added param (for shimmer animation) |
204
+ | `isLoading` | `boolean` | Fetch in progress |
205
+ | `isReady` | `boolean` | Server indicates query is complete |
206
+ | `error` | `Error \| null` | Last fetch error |
207
+
208
+ ### Actions
120
209
 
121
- ```ts
122
- new AIAutocomplete(container: HTMLElement, options?: CoreOptions)
123
- ```
210
+ | Method | Description |
211
+ |---|---|
212
+ | `handleTextChange(value)` | Call when user types. Handles capitalization, param reconciliation, filter updates. |
213
+ | `handleKeyDown(event)` | Forward keyboard events. Handles arrow nav, Enter, Tab, Escape. |
214
+ | `setFocused(focused)` | Notify the library that the input has focus. Required for `dropdownTrigger: "auto"` in Tier 2/3 — the dropdown only opens while focused. |
215
+ | `selectOption(option)` | Select a dropdown option. Updates text, creates completed param, triggers shimmer. |
216
+ | `setActiveDropdownIndex(index)` | Set the highlighted option (for mouse hover). |
217
+ | `setActivePill(index)` | Reorder pills — moves pill at `index` to front (active). |
218
+ | `removeLastParam()` | Remove last completed param, restore it as a pill. |
219
+ | `clearNewParamId()` | Clear shimmer animation state. |
220
+ | `reset()` | Clear all state, re-fetch initial suggestions. |
221
+ | `setValue(text)` | Set text (controlled mode). |
222
+ | `setCompletedParams(params)` | Set completed params (controlled mode). |
223
+ | `setMode(mode)` | Switch color mode at runtime. |
224
+ | `update(opts)` | Update multiple options at once (e.g. `animations`, `pillPlacement`, `dropdownTrigger`, `optionsPosition`). |
225
+ | `getState()` | Get a snapshot of the current state. Useful for initial read before subscribing. |
226
+ | `subscribe(listener)` | Subscribe to state changes (fires after derived state settles). Returns unsubscribe function. |
227
+ | `destroy()` | Cleanup — abort fetches, clear timers, remove listeners. |
124
228
 
125
- ### `CoreOptions`
229
+ ### Building a framework wrapper
126
230
 
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. |
231
+ The pattern for any framework:
232
+
233
+ 1. Create a headless instance on mount
234
+ 2. Subscribe to state map to framework reactivity (`useState`, `ref()`, `writable()`)
235
+ 3. Render your own components using the state
236
+ 4. Forward user events (input, keydown, click) core actions
237
+ 5. Destroy on unmount
238
+
239
+ ---
240
+
241
+ ## API Reference
144
242
 
145
243
  ### `APIConfig`
146
244
 
@@ -172,100 +270,104 @@ A discriminated union: `APIKeyConfig | AccessTokenConfig`.
172
270
  }
173
271
  ```
174
272
 
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.
273
+ 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. Tokens are refreshed proactively 30 seconds before `expiresAt`.
176
274
 
177
- ### Instance Methods
275
+ ### `AutocompleteResult`
178
276
 
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. |
277
+ The object passed to `onSubmit`:
278
+
279
+ | Field | Type | Description |
280
+ |---|---|---|
281
+ | `query` | `string` | Plain text as the user sees it. |
282
+ | `raw_query` | `string` | Text with placeholder tokens (e.g. `"Create a {{TASK_1}}"`). |
283
+ | `completed_params` | `CompletedParam[]` | Array of filled parameter values. |
193
284
 
194
285
  ### Event Subscription
195
286
 
196
287
  ```ts
197
- const off = instance.on("submit", (result) => { ... });
288
+ const off = ac.on("submit", (result) => { ... });
198
289
  off(); // unsubscribe
199
290
  ```
200
291
 
201
- Events: `submit`, `error`, `change`, `paramsChange`, `stateChange`.
292
+ Events: `submit`, `error`, `change`, `paramsChange`, `focus`, `blur`.
202
293
 
203
294
  Constructor callbacks (`onSubmit`, `onChange`, etc.) are equivalent to calling `on()`.
204
295
 
205
- ### `AutocompleteResult`
296
+ **`subscribe()` vs `on()`:** Use `on()` for specific events (submit, error, text change). Use `subscribe()` (Tier 3) for full state updates — it fires after derived state is settled, making it safe for rendering.
206
297
 
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. |
298
+ ---
214
299
 
215
300
  ## CSS Customization
216
301
 
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.
302
+ Styles are auto-injected at runtime (Tier 1 and Tier 2). No CSS import needed. The component ships built-in light and dark defaults.
218
303
 
219
304
  ### CSS Variables
220
305
 
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`.
306
+ Override these on the container element. All built-in defaults use `:where()` (zero specificity) your overrides always win without `!important`.
222
307
 
223
308
  | Variable | Default (light) | Default (dark) | Description |
224
309
  |---|---|---|---|
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 |
310
+ | `--aia-pill-bg` | `#bdbdbd` | `#bdbdbd` | Pill background |
311
+ | `--aia-pill-color` | `#000000` | `#ffffff` | Pill text |
312
+ | `--aia-pill-font-size` | `19px` | `19px` | Pill font size |
313
+ | `--aia-option-bg` | `transparent` | `transparent` | Option background (highlighted) |
314
+ | `--aia-option-color` | `#000000` | `#ffffff` | Option text |
315
+ | `--aia-option-color-selected` | `#000000` | `#ffffff` | Highlighted option text |
316
+ | `--aia-option-font-size` | `19px` | `19px` | Option font size |
317
+ | `--aia-written-text-color` | `#000000` | `#ffffff` | Input text |
318
+ | `--aia-written-text-font-size` | `19px` | `19px` | Input text font size |
319
+ | `--aia-caret-color` | `--aia-written-text-color` | `--aia-written-text-color` | Textarea caret color. Override independently of input text color. |
320
+ | `--aia-submit-bg` | `#000000` | `#ffffff` | Submit button background |
321
+ | `--aia-submit-color` | `#ffffff` | `#000000` | Submit button icon color |
322
+ | `--aia-dropdown-bg` | — | — | Optional bg color the dropdown's "glass" rim shadow tints toward. Set this to the page background behind the dropdown so the bottom-corner glow blends seamlessly. |
323
+ | `--aia-scrollbar-thumb` | `rgba(0, 0, 0, 0.3)` | `rgba(0, 0, 0, 0.3)` | Color of the option list's scrollbar thumb (Firefox + WebKit). |
324
+ | `--aia-streak-rgb` | `99, 102, 241` | `255, 255, 255` | Comma-separated RGB triplet used to tint the option-selection streak animation (consumed via `rgba(var(--aia-streak-rgb), …)`). |
325
+ | `--aia-streak-glass-bg` | `rgba(99, 102, 241, 0.1)` | `rgba(255, 255, 255, 0.1)` | Background fill for the streak's glass-pill effect. |
234
326
 
235
327
  ### Per-mode Overrides
236
328
 
237
329
  ```css
238
330
  #my-autocomplete[data-mode="light"] {
239
- --ac-pill-bg: #e2e8f0;
240
- --ac-written-text-color: #1e293b;
331
+ --aia-pill-bg: #e2e8f0;
241
332
  }
242
-
243
333
  #my-autocomplete[data-mode="dark"] {
244
- --ac-pill-bg: #334155;
245
- --ac-written-text-color: #f8fafc;
334
+ --aia-pill-bg: #334155;
246
335
  }
247
336
  ```
248
337
 
249
338
  ### Live Preview
250
339
 
251
- CSS variables update instantly — great for theme editors:
252
-
253
340
  ```ts
254
341
  document.getElementById("my-autocomplete")
255
- .style.setProperty("--ac-pill-bg", newColor);
342
+ .style.setProperty("--aia-pill-bg", newColor);
256
343
  ```
257
344
 
258
- ## Keyboard Behavior
345
+ ### Selector Hooks
259
346
 
260
- | Key | Behavior |
347
+ For styling beyond the CSS variables, target these stable `data-aia-*` attributes:
348
+
349
+ | Attribute | Element |
261
350
  |---|---|
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. |
351
+ | `[data-aia-editor]` | Editor area wrapping the textarea + overlay |
352
+ | `[data-aia-textarea]` | The `<textarea>` |
353
+ | `[data-aia-submit]` | Submit button |
354
+ | `[data-aia-pill]` | Each unfilled-suggestion pill |
355
+ | `[data-aia-pillbar]` | Pill bar container inside the dropdown |
356
+ | `[data-aia-option]` | Each suggestion option |
357
+ | `[data-aia-dropdown]` | The dropdown root (listbox) |
358
+
359
+ The vanilla core also exposes stable BEM class names (`magicx-aia-*`); both can be used.
360
+
361
+ ```css
362
+ /* Solid (non-glass) dropdown */
363
+ #my-autocomplete [data-aia-dropdown] {
364
+ background: #fff;
365
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
366
+ backdrop-filter: none;
367
+ }
368
+ ```
369
+
370
+ ---
269
371
 
270
372
  ## Option Overrides
271
373
 
@@ -286,6 +388,8 @@ new AIAutocomplete(el, {
286
388
  });
287
389
  ```
288
390
 
391
+ ---
392
+
289
393
  ## License
290
394
 
291
395
  Private package. All rights reserved.
package/index.d.mts CHANGED
@@ -43,7 +43,7 @@ interface APIConfigBase {
43
43
  }
44
44
  interface APIKeyConfig extends APIConfigBase {
45
45
  type?: "apiKey";
46
- apiKey: string;
46
+ apiKey?: string;
47
47
  authScheme?: "Bearer" | "Basic";
48
48
  }
49
49
  interface AccessTokenConfig extends APIConfigBase {
@@ -64,14 +64,6 @@ interface AutocompleteResult {
64
64
  completed_params: CompletedParam[];
65
65
  }
66
66
 
67
- type Listener<S> = (next: S, prev: S) => void;
68
- interface Store<S> {
69
- get: () => S;
70
- set: (patch: Partial<S> | ((s: S) => Partial<S>)) => void;
71
- subscribe: (listener: Listener<S>) => () => void;
72
- }
73
- declare function createStore<S>(initial: S): Store<S>;
74
-
75
67
  /** Internal state managed by the store. */
76
68
  interface CoreState {
77
69
  text: string;
@@ -92,60 +84,67 @@ interface CoreState {
92
84
  pillTapped: boolean;
93
85
  skipNextFetch: boolean;
94
86
  lastRawQuery: string;
87
+ isFocused: boolean;
95
88
  }
89
+ /**
90
+ * Render mode:
91
+ * - "full" (default) — renders input + dropdown + pills. Tier 1.
92
+ * - "dropdown" — renders only the dropdown with pills. Consumer owns the input. Tier 2.
93
+ * - "headless" — no DOM. State + controllers only. For framework wrappers.
94
+ */
95
+ type RenderMode = "full" | "dropdown" | "headless";
96
96
  /** Options passed to the AIAutocomplete constructor. */
97
97
  interface CoreOptions {
98
98
  apiConfig?: APIConfig;
99
99
  optionOverrides?: OptionOverrides;
100
100
  maskCompletedText?: boolean;
101
- placeholder?: string;
102
101
  columns?: number;
103
- typewriterEffect?: boolean;
104
- pillPlacement?: "inline" | "dropdown";
102
+ /** Where pills render in "full" mode. Ignored in "dropdown" mode (always in dropdown). */
103
+ pillPlacement?: "inline" | "dropdown" | "hidden";
105
104
  mode?: AppearanceMode;
106
105
  optionsPosition?: "above" | "below";
107
106
  animations?: boolean;
107
+ /** When the dropdown appears. "auto" = shows when options available. "manual" = only on pill tap. "hidden" = never. Default: "auto". */
108
+ dropdownTrigger?: "auto" | "manual" | "hidden";
109
+ /** When true (default), the dropdown closes when the input loses focus. Set to false to keep it open whenever options are available, regardless of focus. */
110
+ closeDropdownOnBlur?: boolean;
111
+ /** Render mode. Default: "full". */
112
+ renderMode?: RenderMode;
113
+ /** Focus the input on mount (Tier 1 / "full" render mode only). Default: true. */
114
+ autoFocus?: boolean;
108
115
  onSubmit?: (result: AutocompleteResult) => void;
109
116
  onError?: (error: Error) => void;
110
117
  onChange?: (text: string) => void;
111
118
  onParamsChange?: (params: CompletedParamState[]) => void;
112
119
  onStateChange?: (state: CoreState) => void;
120
+ /** Called when the input gains focus (or `setFocused(true)` is called). */
121
+ onFocus?: () => void;
122
+ /** Called when the input loses focus (or `setFocused(false)` is called). */
123
+ onBlur?: () => void;
113
124
  value?: string;
114
125
  completedParams?: CompletedParamState[];
115
126
  }
116
127
 
117
- interface KeyboardContext {
118
- columns: number;
119
- listboxId: string;
120
- onSubmit?: (result: AutocompleteResult) => void;
121
- selectOption: (option: SuggestionOption) => void;
122
- }
123
- declare class KeyboardController {
124
- private store;
125
- private ctx;
126
- constructor(store: Store<CoreState>, ctx: KeyboardContext);
127
- handleKeyDown(e: KeyboardEvent): void;
128
- private getTappableIndices;
129
- private clickOrSelect;
130
- /** Delegate to pills controller logic inline (avoids circular dep) */
131
- private pillsSetActivePill;
132
- }
133
-
134
128
  declare class AIAutocomplete {
135
129
  private store;
136
- private listboxId;
130
+ private _listboxId;
137
131
  private opts;
138
132
  private fetchController;
139
- keyboardController: KeyboardController;
133
+ private keyboardController;
140
134
  private pillsController;
141
135
  private modeController;
142
136
  private container;
143
137
  private unsubscribers;
144
138
  private derivedInProgress;
139
+ private renderMode;
145
140
  private domRefs;
141
+ private dropdownRefs;
146
142
  private newParamTimer;
143
+ private suggestionRemovalTimer;
144
+ private externalListeners;
147
145
  constructor(container: HTMLElement, opts?: CoreOptions);
148
146
  focus(): void;
147
+ blur(): void;
149
148
  reset(): void;
150
149
  destroy(): void;
151
150
  setMode(mode: AppearanceMode): void;
@@ -154,15 +153,35 @@ declare class AIAutocomplete {
154
153
  setActivePill(index: number): void;
155
154
  removeLastParam(): void;
156
155
  clearNewParamId(): void;
156
+ setActiveDropdownIndex(index: number): void;
157
+ handleTextChange(value: string): void;
158
+ handleKeyDown(e: KeyboardEvent): void;
159
+ setFocused(focused: boolean): void;
160
+ /** Subscribe to state changes (fires after derived state is settled). Returns unsubscribe function. */
161
+ subscribe(listener: (state: CoreState) => void): () => void;
157
162
  getState(): CoreState;
163
+ get listboxId(): string;
158
164
  get isReady(): boolean;
159
165
  on(event: string, callback: (...args: unknown[]) => void): () => void;
160
166
  update(opts: Partial<CoreOptions>): void;
161
- private selectOption;
167
+ selectOption(option: SuggestionOption): void;
162
168
  private recomputeDerived;
163
169
  private setupContainer;
164
- private buildAndRender;
170
+ private buildAndRenderFull;
171
+ private buildAndRenderDropdown;
172
+ /** Batched render subscriber — coalesces multiple store.set calls into one DOM update. */
173
+ private subscribeBatchedRender;
174
+ /** Auto-clear newParamId after shimmer animation. */
175
+ private subscribeNewParamTimer;
165
176
  private handleChange;
166
177
  }
167
178
 
168
- export { AIAutocomplete, type APIConfig, type APIKeyConfig, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteResult, type CompletedParam, type CompletedParamState, type CoreOptions, type CoreState, type Segment, type Store, type Suggestion, type SuggestionOption, createStore };
179
+ type Listener<S> = (next: S, prev: S) => void;
180
+ interface Store<S> {
181
+ get: () => S;
182
+ set: (patch: Partial<S> | ((s: S) => Partial<S>)) => void;
183
+ subscribe: (listener: Listener<S>) => () => void;
184
+ }
185
+ declare function createStore<S>(initial: S): Store<S>;
186
+
187
+ export { AIAutocomplete, type APIConfig, type APIKeyConfig, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteResult, type CompletedParam, type CompletedParamState, type CoreOptions, type CoreState, type RenderMode, type Segment, type Store, type Suggestion, type SuggestionOption, createStore };