@magicx-eng/ai-autocomplete-vanilla 0.1.0 → 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 ADDED
@@ -0,0 +1,386 @@
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
+ - **Three tiers of integration** — full drop-in component, dropdown-only, or fully headless
8
+ - **Zero dependencies** — no React, no framework, no virtual DOM
9
+ - **Pill-based input** — inline pills for unfilled parameters, completed text for filled ones
10
+ - **Pill placement** — render pills inline in the input or inside the dropdown
11
+ - **Light/dark mode** — built-in themes with `prefers-color-scheme` support, fully overridable via CSS variables
12
+ - **Access token auth** — short-lived tokens with automatic refresh, single-flight deduplication, and 401 retry
13
+ - **Keyboard navigation** — arrow keys, enter to submit, tab to autocomplete
14
+ - **Client-side filtering** — instant substring filtering on every keystroke
15
+ - **Option overrides** — inject or dynamically generate client-side options per suggestion type
16
+ - **Controlled & uncontrolled** — works out of the box or integrates with external state
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
+ ## Three Tiers
30
+
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 |
36
+
37
+ ---
38
+
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
+ });
57
+ ```
58
+
59
+ No CSS import needed — styles are auto-injected on first instantiation.
60
+
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
+
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
+ ```
102
+
103
+ ---
104
+
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
110
+ import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
111
+
112
+ const input = document.getElementById("my-input");
113
+ const dropdownContainer = document.getElementById("my-dropdown");
114
+
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),
123
+ });
124
+
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));
130
+ ```
131
+
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.
133
+
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.
135
+
136
+ ---
137
+
138
+ ## Tier 3: Headless
139
+
140
+ No DOM at all. You get state, controllers, and derived data. You render everything yourself.
141
+
142
+ This is what framework wrappers (React, Vue, Svelte) use under the hood.
143
+
144
+ ```ts
145
+ import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
146
+
147
+ const ac = new AIAutocomplete(document.createElement("div"), {
148
+ renderMode: "headless",
149
+ apiConfig: { apiKey: "your_api_key" },
150
+ onSubmit: (result) => console.log(result),
151
+ });
152
+
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();
179
+ ```
180
+
181
+ ### State shape (`CoreState`)
182
+
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
200
+
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. |
219
+
220
+ ### Building a framework wrapper
221
+
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
233
+
234
+ ### `APIConfig`
235
+
236
+ A discriminated union: `APIKeyConfig | AccessTokenConfig`.
237
+
238
+ #### API Key Mode (default)
239
+
240
+ ```ts
241
+ {
242
+ apiKey: "your_api_key",
243
+ authScheme: "Bearer", // or "Basic"
244
+ endpoint: "/ac/suggest", // optional
245
+ appIdentifier: "my-app", // optional
246
+ headers: { "X-Custom": "v" }, // optional
247
+ }
248
+ ```
249
+
250
+ #### Access Token Mode
251
+
252
+ ```ts
253
+ {
254
+ type: "accessToken",
255
+ getAccessToken: async () => {
256
+ const res = await fetch("/api/ac-token");
257
+ const { access_token, expires_at } = await res.json();
258
+ return { accessToken: access_token, expiresAt: expires_at };
259
+ },
260
+ endpoint: "/ac/suggest", // optional
261
+ }
262
+ ```
263
+
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`.
265
+
266
+ ### `AutocompleteResult`
267
+
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. |
275
+
276
+ ### Event Subscription
277
+
278
+ ```ts
279
+ const off = ac.on("submit", (result) => { ... });
280
+ off(); // unsubscribe
281
+ ```
282
+
283
+ Events: `submit`, `error`, `change`, `paramsChange`.
284
+
285
+ Constructor callbacks (`onSubmit`, `onChange`, etc.) are equivalent to calling `on()`.
286
+
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.
288
+
289
+ ---
290
+
291
+ ## CSS Customization
292
+
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.
294
+
295
+ ### CSS Variables
296
+
297
+ Override these on the container element. All built-in defaults use `:where()` (zero specificity) — your overrides always win without `!important`.
298
+
299
+ | Variable | Default (light) | Default (dark) | Description |
300
+ |---|---|---|---|
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. |
317
+
318
+ ### Per-mode Overrides
319
+
320
+ ```css
321
+ #my-autocomplete[data-mode="light"] {
322
+ --aia-pill-bg: #e2e8f0;
323
+ }
324
+ #my-autocomplete[data-mode="dark"] {
325
+ --aia-pill-bg: #334155;
326
+ }
327
+ ```
328
+
329
+ ### Live Preview
330
+
331
+ ```ts
332
+ document.getElementById("my-autocomplete")
333
+ .style.setProperty("--aia-pill-bg", newColor);
334
+ ```
335
+
336
+ ### Selector Hooks
337
+
338
+ For styling beyond the CSS variables, target these stable `data-aia-*` attributes:
339
+
340
+ | Attribute | Element |
341
+ |---|---|
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
+ ---
362
+
363
+ ## Option Overrides
364
+
365
+ ```ts
366
+ new AIAutocomplete(el, {
367
+ optionOverrides: {
368
+ account: () => [
369
+ { text: "Savings", is_tappable: true, kind: null },
370
+ { text: "Checking", is_tappable: true, kind: null },
371
+ ],
372
+ value: (query) => {
373
+ const digits = query.replace(/\D/g, "");
374
+ if (!digits) return [{ text: "$100", is_tappable: true, kind: null }];
375
+ return [{ text: `$${digits}`, is_tappable: true, kind: null }];
376
+ },
377
+ },
378
+ onSubmit: handleSubmit,
379
+ });
380
+ ```
381
+
382
+ ---
383
+
384
+ ## License
385
+
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 };
package/index.d.ts 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 };