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

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,210 @@ 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
54
62
 
55
- ### Vue
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
+
81
+ // Events
82
+ onSubmit: (result) => { ... },
83
+ onError: (error) => { ... },
84
+ onChange: (text) => { ... },
85
+ onParamsChange: (params) => { ... },
86
+
87
+ // Controlled mode
88
+ value: "initial text",
89
+ completedParams: [],
90
+ });
91
+ ```
92
+
93
+ ### Methods
94
+
95
+ ```ts
96
+ ac.focus(); // Focus the input
97
+ ac.reset(); // Clear everything, re-fetch
98
+ ac.destroy(); // Remove DOM, listeners, timers
99
+ ac.setMode("dark"); // Switch color mode
100
+ ac.update({ animations: false, optionsPosition: "above" });
101
+ ```
56
102
 
57
- ```vue
58
- <template>
59
- <div ref="acRef" />
60
- </template>
103
+ ---
61
104
 
62
- <script setup>
63
- import { ref, onMounted, onUnmounted } from "vue";
105
+ ## Tier 2: Dropdown Only
106
+
107
+ You own the input. The library renders the dropdown with pills inside a container you provide.
108
+
109
+ ```ts
64
110
  import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
65
111
 
66
- const acRef = ref(null);
67
- let instance;
112
+ const input = document.getElementById("my-input");
113
+ const dropdownContainer = document.getElementById("my-dropdown");
68
114
 
69
- onMounted(() => {
70
- instance = new AIAutocomplete(acRef.value, {
71
- apiConfig: { endpoint: "/ac/suggest", apiKey: "your_key" },
72
- onSubmit: (result) => console.log(result),
73
- });
115
+ const ac = new AIAutocomplete(dropdownContainer, {
116
+ renderMode: "dropdown",
117
+ apiConfig: { apiKey: "your_api_key" },
118
+ onChange: (text) => {
119
+ // Keep your input in sync when the library updates text (e.g. option selected)
120
+ if (input.value !== text) input.value = text;
121
+ },
122
+ onSubmit: (result) => console.log(result),
74
123
  });
75
124
 
76
- onUnmounted(() => instance?.destroy());
77
- </script>
125
+ // Wire your input → library
126
+ input.addEventListener("input", () => ac.handleTextChange(input.value));
127
+ input.addEventListener("keydown", (e) => ac.handleKeyDown(e));
128
+ input.addEventListener("focus", () => ac.setFocused(true));
129
+ input.addEventListener("blur", () => ac.setFocused(false));
78
130
  ```
79
131
 
80
- ### Svelte
132
+ 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
133
 
82
- ```svelte
83
- <script>
84
- import { onMount, onDestroy } from "svelte";
85
- import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
134
+ > 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
135
 
87
- let el;
88
- let instance;
136
+ ---
89
137
 
90
- onMount(() => {
91
- instance = new AIAutocomplete(el, {
92
- apiConfig: { endpoint: "/ac/suggest", apiKey: "your_key" },
93
- onSubmit: (result) => console.log(result),
94
- });
95
- });
138
+ ## Tier 3: Headless
96
139
 
97
- onDestroy(() => instance?.destroy());
98
- </script>
140
+ No DOM at all. You get state, controllers, and derived data. You render everything yourself.
99
141
 
100
- <div bind:this={el}></div>
101
- ```
142
+ This is what framework wrappers (React, Vue, Svelte) use under the hood.
102
143
 
103
- ### Plain HTML (CDN)
144
+ ```ts
145
+ import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
104
146
 
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";
147
+ const ac = new AIAutocomplete(document.createElement("div"), {
148
+ renderMode: "headless",
149
+ apiConfig: { apiKey: "your_api_key" },
150
+ onSubmit: (result) => console.log(result),
151
+ });
109
152
 
110
- new AIAutocomplete(document.getElementById("ac"), {
111
- apiConfig: { endpoint: "/ac/suggest", apiKey: "your_key" },
112
- onSubmit: (result) => console.log(result),
113
- });
114
- </script>
153
+ // Read state
154
+ const state = ac.getState();
155
+ // state.text, state.completedParams, state.suggestions,
156
+ // state.segments, state.filteredOptions, state.actionableSuggestions,
157
+ // state.placeholderText, state.isDropdownOpen, state.activeDropdownIndex,
158
+ // state.newParamId, state.isLoading, state.isReady, state.error
159
+
160
+ // Subscribe to changes (fires after derived state is settled)
161
+ const unsub = ac.subscribe((state) => {
162
+ renderMyInput(state.text, state.placeholderText);
163
+ renderMyPills(state.actionableSuggestions);
164
+ renderMyDropdown(state.filteredOptions, state.activeDropdownIndex, state.isDropdownOpen);
165
+ renderMySegments(state.segments, state.newParamId);
166
+ });
167
+
168
+ // Forward user actions → library
169
+ myInput.addEventListener("input", () => ac.handleTextChange(myInput.value));
170
+ myInput.addEventListener("keydown", (e) => ac.handleKeyDown(e));
171
+ myInput.addEventListener("focus", () => ac.setFocused(true));
172
+ myInput.addEventListener("blur", () => ac.setFocused(false));
173
+ myOptionEl.addEventListener("click", () => ac.selectOption(option));
174
+ myPillEl.addEventListener("click", () => ac.setActivePill(index));
175
+
176
+ // Cleanup
177
+ ac.destroy();
178
+ unsub();
115
179
  ```
116
180
 
117
- ## API Reference
181
+ ### State shape (`CoreState`)
118
182
 
119
- ### Constructor
183
+ | Field | Type | Description |
184
+ |---|---|---|
185
+ | `text` | `string` | Current input text |
186
+ | `completedParams` | `CompletedParamState[]` | Filled parameters |
187
+ | `suggestions` | `Suggestion[]` | All suggestions from server (including placeholder type) |
188
+ | `actionableSuggestions` | `Suggestion[]` | Non-placeholder suggestions (the pills) |
189
+ | `filteredOptions` | `SuggestionOption[]` | Options for the active suggestion, filtered by current query |
190
+ | `segments` | `Segment[]` | Input text split into typed text vs completed params — for overlay rendering |
191
+ | `placeholderText` | `string` | Placeholder text from server suggestions (joined `placeholder`-type suggestion texts) |
192
+ | `activeDropdownIndex` | `number` | Highlighted option index (-1 = none) |
193
+ | `isDropdownOpen` | `boolean` | Whether the dropdown should be visible |
194
+ | `newParamId` | `string \| null` | ID of the most recently added param (for shimmer animation) |
195
+ | `isLoading` | `boolean` | Fetch in progress |
196
+ | `isReady` | `boolean` | Server indicates query is complete |
197
+ | `error` | `Error \| null` | Last fetch error |
198
+
199
+ ### Actions
120
200
 
121
- ```ts
122
- new AIAutocomplete(container: HTMLElement, options?: CoreOptions)
123
- ```
201
+ | Method | Description |
202
+ |---|---|
203
+ | `handleTextChange(value)` | Call when user types. Handles capitalization, param reconciliation, filter updates. |
204
+ | `handleKeyDown(event)` | Forward keyboard events. Handles arrow nav, Enter, Tab, Escape. |
205
+ | `setFocused(focused)` | Notify the library that the input has focus. Required for `dropdownTrigger: "auto"` in Tier 2/3 — the dropdown only opens while focused. |
206
+ | `selectOption(option)` | Select a dropdown option. Updates text, creates completed param, triggers shimmer. |
207
+ | `setActiveDropdownIndex(index)` | Set the highlighted option (for mouse hover). |
208
+ | `setActivePill(index)` | Reorder pills — moves pill at `index` to front (active). |
209
+ | `removeLastParam()` | Remove last completed param, restore it as a pill. |
210
+ | `clearNewParamId()` | Clear shimmer animation state. |
211
+ | `reset()` | Clear all state, re-fetch initial suggestions. |
212
+ | `setValue(text)` | Set text (controlled mode). |
213
+ | `setCompletedParams(params)` | Set completed params (controlled mode). |
214
+ | `setMode(mode)` | Switch color mode at runtime. |
215
+ | `update(opts)` | Update multiple options at once (e.g. `animations`, `pillPlacement`, `dropdownTrigger`, `optionsPosition`). |
216
+ | `getState()` | Get a snapshot of the current state. Useful for initial read before subscribing. |
217
+ | `subscribe(listener)` | Subscribe to state changes (fires after derived state settles). Returns unsubscribe function. |
218
+ | `destroy()` | Cleanup — abort fetches, clear timers, remove listeners. |
124
219
 
125
- ### `CoreOptions`
220
+ ### Building a framework wrapper
126
221
 
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. |
222
+ The pattern for any framework:
223
+
224
+ 1. Create a headless instance on mount
225
+ 2. Subscribe to state map to framework reactivity (`useState`, `ref()`, `writable()`)
226
+ 3. Render your own components using the state
227
+ 4. Forward user events (input, keydown, click) core actions
228
+ 5. Destroy on unmount
229
+
230
+ ---
231
+
232
+ ## API Reference
144
233
 
145
234
  ### `APIConfig`
146
235
 
@@ -172,100 +261,104 @@ A discriminated union: `APIKeyConfig | AccessTokenConfig`.
172
261
  }
173
262
  ```
174
263
 
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.
264
+ 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
265
 
177
- ### Instance Methods
266
+ ### `AutocompleteResult`
178
267
 
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. |
268
+ The object passed to `onSubmit`:
269
+
270
+ | Field | Type | Description |
271
+ |---|---|---|
272
+ | `query` | `string` | Plain text as the user sees it. |
273
+ | `raw_query` | `string` | Text with placeholder tokens (e.g. `"Create a {{TASK_1}}"`). |
274
+ | `completed_params` | `CompletedParam[]` | Array of filled parameter values. |
193
275
 
194
276
  ### Event Subscription
195
277
 
196
278
  ```ts
197
- const off = instance.on("submit", (result) => { ... });
279
+ const off = ac.on("submit", (result) => { ... });
198
280
  off(); // unsubscribe
199
281
  ```
200
282
 
201
- Events: `submit`, `error`, `change`, `paramsChange`, `stateChange`.
283
+ Events: `submit`, `error`, `change`, `paramsChange`.
202
284
 
203
285
  Constructor callbacks (`onSubmit`, `onChange`, etc.) are equivalent to calling `on()`.
204
286
 
205
- ### `AutocompleteResult`
287
+ **`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
288
 
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. |
289
+ ---
214
290
 
215
291
  ## CSS Customization
216
292
 
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.
293
+ 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
294
 
219
295
  ### CSS Variables
220
296
 
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`.
297
+ Override these on the container element. All built-in defaults use `:where()` (zero specificity) your overrides always win without `!important`.
222
298
 
223
299
  | Variable | Default (light) | Default (dark) | Description |
224
300
  |---|---|---|---|
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 |
301
+ | `--aia-pill-bg` | `#bdbdbd` | `#bdbdbd` | Pill background |
302
+ | `--aia-pill-color` | `#000000` | `#ffffff` | Pill text |
303
+ | `--aia-pill-font-size` | `19px` | `19px` | Pill font size |
304
+ | `--aia-option-bg` | `transparent` | `transparent` | Option background (highlighted) |
305
+ | `--aia-option-color` | `#000000` | `#ffffff` | Option text |
306
+ | `--aia-option-color-selected` | `#000000` | `#ffffff` | Highlighted option text |
307
+ | `--aia-option-font-size` | `19px` | `19px` | Option font size |
308
+ | `--aia-written-text-color` | `#000000` | `#ffffff` | Input text |
309
+ | `--aia-written-text-font-size` | `19px` | `19px` | Input text font size |
310
+ | `--aia-caret-color` | `--aia-written-text-color` | `--aia-written-text-color` | Textarea caret color. Override independently of input text color. |
311
+ | `--aia-submit-bg` | `#000000` | `#ffffff` | Submit button background |
312
+ | `--aia-submit-color` | `#ffffff` | `#000000` | Submit button icon color |
313
+ | `--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. |
314
+ | `--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). |
315
+ | `--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), …)`). |
316
+ | `--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
317
 
235
318
  ### Per-mode Overrides
236
319
 
237
320
  ```css
238
321
  #my-autocomplete[data-mode="light"] {
239
- --ac-pill-bg: #e2e8f0;
240
- --ac-written-text-color: #1e293b;
322
+ --aia-pill-bg: #e2e8f0;
241
323
  }
242
-
243
324
  #my-autocomplete[data-mode="dark"] {
244
- --ac-pill-bg: #334155;
245
- --ac-written-text-color: #f8fafc;
325
+ --aia-pill-bg: #334155;
246
326
  }
247
327
  ```
248
328
 
249
329
  ### Live Preview
250
330
 
251
- CSS variables update instantly — great for theme editors:
252
-
253
331
  ```ts
254
332
  document.getElementById("my-autocomplete")
255
- .style.setProperty("--ac-pill-bg", newColor);
333
+ .style.setProperty("--aia-pill-bg", newColor);
256
334
  ```
257
335
 
258
- ## Keyboard Behavior
336
+ ### Selector Hooks
259
337
 
260
- | Key | Behavior |
338
+ For styling beyond the CSS variables, target these stable `data-aia-*` attributes:
339
+
340
+ | Attribute | Element |
261
341
  |---|---|
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. |
342
+ | `[data-aia-editor]` | Editor area wrapping the textarea + overlay |
343
+ | `[data-aia-textarea]` | The `<textarea>` |
344
+ | `[data-aia-submit]` | Submit button |
345
+ | `[data-aia-pill]` | Each unfilled-suggestion pill |
346
+ | `[data-aia-pillbar]` | Pill bar container inside the dropdown |
347
+ | `[data-aia-option]` | Each suggestion option |
348
+ | `[data-aia-dropdown]` | The dropdown root (listbox) |
349
+
350
+ The vanilla core also exposes stable BEM class names (`magicx-aia-*`); both can be used.
351
+
352
+ ```css
353
+ /* Solid (non-glass) dropdown */
354
+ #my-autocomplete [data-aia-dropdown] {
355
+ background: #fff;
356
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
357
+ backdrop-filter: none;
358
+ }
359
+ ```
360
+
361
+ ---
269
362
 
270
363
  ## Option Overrides
271
364
 
@@ -286,6 +379,8 @@ new AIAutocomplete(el, {
286
379
  });
287
380
  ```
288
381
 
382
+ ---
383
+
289
384
  ## License
290
385
 
291
386
  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,19 +84,30 @@ 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
+ /** Render mode. Default: "full". */
110
+ renderMode?: RenderMode;
108
111
  onSubmit?: (result: AutocompleteResult) => void;
109
112
  onError?: (error: Error) => void;
110
113
  onChange?: (text: string) => void;
@@ -114,36 +117,23 @@ interface CoreOptions {
114
117
  completedParams?: CompletedParamState[];
115
118
  }
116
119
 
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
120
  declare class AIAutocomplete {
135
121
  private store;
136
- private listboxId;
122
+ private _listboxId;
137
123
  private opts;
138
124
  private fetchController;
139
- keyboardController: KeyboardController;
125
+ private keyboardController;
140
126
  private pillsController;
141
127
  private modeController;
142
128
  private container;
143
129
  private unsubscribers;
144
130
  private derivedInProgress;
131
+ private renderMode;
145
132
  private domRefs;
133
+ private dropdownRefs;
146
134
  private newParamTimer;
135
+ private suggestionRemovalTimer;
136
+ private externalListeners;
147
137
  constructor(container: HTMLElement, opts?: CoreOptions);
148
138
  focus(): void;
149
139
  reset(): void;
@@ -154,15 +144,35 @@ declare class AIAutocomplete {
154
144
  setActivePill(index: number): void;
155
145
  removeLastParam(): void;
156
146
  clearNewParamId(): void;
147
+ setActiveDropdownIndex(index: number): void;
148
+ handleTextChange(value: string): void;
149
+ handleKeyDown(e: KeyboardEvent): void;
150
+ setFocused(focused: boolean): void;
151
+ /** Subscribe to state changes (fires after derived state is settled). Returns unsubscribe function. */
152
+ subscribe(listener: (state: CoreState) => void): () => void;
157
153
  getState(): CoreState;
154
+ get listboxId(): string;
158
155
  get isReady(): boolean;
159
156
  on(event: string, callback: (...args: unknown[]) => void): () => void;
160
157
  update(opts: Partial<CoreOptions>): void;
161
- private selectOption;
158
+ selectOption(option: SuggestionOption): void;
162
159
  private recomputeDerived;
163
160
  private setupContainer;
164
- private buildAndRender;
161
+ private buildAndRenderFull;
162
+ private buildAndRenderDropdown;
163
+ /** Batched render subscriber — coalesces multiple store.set calls into one DOM update. */
164
+ private subscribeBatchedRender;
165
+ /** Auto-clear newParamId after shimmer animation. */
166
+ private subscribeNewParamTimer;
165
167
  private handleChange;
166
168
  }
167
169
 
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 };
170
+ type Listener<S> = (next: S, prev: S) => void;
171
+ interface Store<S> {
172
+ get: () => S;
173
+ set: (patch: Partial<S> | ((s: S) => Partial<S>)) => void;
174
+ subscribe: (listener: Listener<S>) => () => void;
175
+ }
176
+ declare function createStore<S>(initial: S): Store<S>;
177
+
178
+ 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 };