@magicx-eng/ai-autocomplete-vanilla 0.1.14 → 0.1.16

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
@@ -6,16 +6,20 @@ A framework-agnostic vanilla JS/TypeScript library that provides a guided AI-pow
6
6
 
7
7
  - **Three tiers of integration** — full drop-in component, dropdown-only, or fully headless
8
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
9
+ - **Rich inline input** — a single `contentEditable` surface: typed text, bold completed params, and inline pills all share one editing context
10
+ - **Pill-based input** — non-editable inline pills for unfilled parameters, bold inline text for completed ones
11
+ - **Instant exact-match bolding** — typing the full text of an option immediately promotes it to a completed param (no debounced fetch wait). Works in the normal typing flow *and* while re-editing an existing completed param.
12
+ - **Re-edit completed params** — tap a bold completed param to replace it; the dropdown re-opens with the cached options the server originally returned for that param
11
13
  - **Light/dark mode** — built-in themes with `prefers-color-scheme` support, fully overridable via CSS variables
12
14
  - **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
15
+ - **Keyboard navigation** — arrow keys, enter to submit, tab to autocomplete, backspace to un-bold the last completed param
14
16
  - **Client-side filtering** — instant substring filtering on every keystroke
15
17
  - **Option overrides** — inject or dynamically generate client-side options per suggestion type
16
18
  - **Controlled & uncontrolled** — works out of the box or integrates with external state
17
- - **Accessible** — ARIA combobox pattern with `role="listbox"`, `aria-activedescendant`
19
+ - **Accessible** — ARIA combobox 1.2 pattern with `role="listbox"`, `aria-activedescendant`
20
+ - **IME-safe** — composition events are buffered so input text is committed once, after composition ends
18
21
  - **Animations** — option selection streak animation, text shimmer on newly added params
22
+ - **Loading skeleton** — while a fetch is in flight, the dropdown and inline pills keep the previous layout (same count and widths) with their text masked and a shimmer pulse. The skeleton is held back until the selection streak animation finishes, so taps don't visually "stutter" into loading.
19
23
  - **Lightweight** — ~10 KB gzipped, styles auto-injected at runtime
20
24
  - **TypeScript first** — full type definitions shipped with the package
21
25
  - **SSR-safe** — no top-level `document`/`window` access
@@ -115,7 +119,13 @@ ac.update({ animations: false, optionsPosition: "above" });
115
119
 
116
120
  > **Tier 1 auto-resets** after both Enter key and built-in submit-button clicks — your `onSubmit` runs, then the SDK clears the input and starts a new session. You don't need to call `ac.reset()` yourself in Tier 1.
117
121
 
118
- > `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.
122
+ > `autoFocus`, `focus()`, and `blur()` only operate on the contentEditable editor 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.
123
+
124
+ > **Backspace into a completed param** — pressing Backspace while the caret is inside (or immediately after) a bold completed param drops the param's "completed" status and removes one grapheme before the caret. The remaining text stays in the editor as plain (un-bold) text so the user can keep editing instead of losing the whole phrase.
125
+
126
+ > **Re-edit a completed param** — tapping a bold completed param enters *re-edit mode*. The dropdown re-opens with the cached options the server originally returned for that param. Typing atomically replaces the bold (and re-promotes it to bold if what you type exactly matches one of the cached options); clicking an option replaces it with the new selection; pressing Escape or moving the caret out exits without changes.
127
+
128
+ > **Caret placement after a completion** — whenever a completed param is added (by any means — option click, exact-match typing, or re-edit), the caret lands right after the trailing space following the bold so typing can continue immediately. A space is inserted if one wasn't already there.
119
129
 
120
130
  #### Custom submit button
121
131
 
@@ -221,12 +231,14 @@ unsub();
221
231
  | `suggestions` | `Suggestion[]` | All suggestions from server (including placeholder type) |
222
232
  | `actionableSuggestions` | `Suggestion[]` | Non-placeholder suggestions (the pills) |
223
233
  | `filteredOptions` | `SuggestionOption[]` | Options for the active suggestion, filtered by current query |
224
- | `segments` | `Segment[]` | Input text split into typed text vs completed params — for overlay rendering |
234
+ | `segments` | `Segment[]` | Input text split into typed text vs completed params — completed segments render as bold `<strong>` runs inside the editor |
225
235
  | `placeholderText` | `string` | Placeholder text from server suggestions (joined `placeholder`-type suggestion texts) |
226
236
  | `activeDropdownIndex` | `number` | Highlighted option index (-1 = none) |
227
237
  | `isDropdownOpen` | `boolean` | Whether the dropdown should be visible |
228
238
  | `newParamId` | `string \| null` | ID of the most recently added param (for shimmer animation) |
229
- | `isLoading` | `boolean` | Fetch in progress |
239
+ | `isLoading` | `boolean` | Fetch in progress. The Tier 1 / Tier 2 renderers gate the loading skeleton additionally on `!inSelectionAnimation` and `!editingParam`. |
240
+ | `inSelectionAnimation` | `boolean` | True for the 500 ms after a user-initiated option tap so the streak animation can finish before the dropdown switches to the loading skeleton. |
241
+ | `editingParam` | `CompletedParamState \| null` | When non-null, the user is re-editing a bold completed param; cached options remain visible and the loading skeleton is suppressed. |
230
242
  | `isReady` | `boolean` | Server indicates query is complete |
231
243
  | `error` | `Error \| null` | Last fetch error |
232
244
 
@@ -240,7 +252,7 @@ unsub();
240
252
  | `selectOption(option)` | Select a dropdown option. Updates text, creates completed param, triggers shimmer. |
241
253
  | `setActiveDropdownIndex(index)` | Set the highlighted option (for mouse hover). |
242
254
  | `setActivePill(index)` | Reorder pills — moves pill at `index` to front (active). |
243
- | `removeLastParam()` | Remove last completed param, restore it as a pill. |
255
+ | `removeLastParam()` | Remove the last completed param from state. The text stays in the input as plain text. |
244
256
  | `clearNewParamId()` | Clear shimmer animation state. |
245
257
  | `reset()` | Clear all state, re-fetch initial suggestions, and start a new session (rotates `session_id`). Call this after handling submit. |
246
258
  | `setValue(text)` | Set text (controlled mode). |
@@ -332,6 +344,7 @@ Override these on the container element. All built-in defaults use `:where()` (z
332
344
 
333
345
  | Variable | Default (light) | Default (dark) | Description |
334
346
  |---|---|---|---|
347
+ | `--aia-font-family` | `inherit` | `inherit` | Font used by the library. Defaults to `inherit` so the library picks up your page's font automatically. Set this to pin a specific font on the library without changing the surrounding page. |
335
348
  | `--aia-pill-bg` | `#bdbdbd` | `#bdbdbd` | Pill background |
336
349
  | `--aia-pill-color` | `#000000` | `#ffffff` | Pill text |
337
350
  | `--aia-pill-font-size` | `19px` | `19px` | Pill font size |
@@ -341,13 +354,14 @@ Override these on the container element. All built-in defaults use `:where()` (z
341
354
  | `--aia-option-font-size` | `19px` | `19px` | Option font size |
342
355
  | `--aia-written-text-color` | `#000000` | `#ffffff` | Input text |
343
356
  | `--aia-written-text-font-size` | `19px` | `19px` | Input text font size |
344
- | `--aia-caret-color` | `--aia-written-text-color` | `--aia-written-text-color` | Textarea caret color. Override independently of input text color. |
357
+ | `--aia-caret-color` | `--aia-written-text-color` | `--aia-written-text-color` | Editor caret color. Override independently of input text color. |
345
358
  | `--aia-submit-bg` | `#000000` | `#ffffff` | Submit button background |
346
359
  | `--aia-submit-color` | `#ffffff` | `#000000` | Submit button icon color |
347
360
  | `--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. |
348
361
  | `--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). |
349
362
  | `--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), …)`). |
350
363
  | `--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. |
364
+ | `--aia-skeleton-bg` | `rgba(189, 189, 189, 0.51)` | `#333539` | Fill color for the loading skeleton bars and the masked text in cached pills/options. |
351
365
 
352
366
  ### Per-mode Overrides
353
367
 
@@ -373,14 +387,17 @@ For styling beyond the CSS variables, target these stable `data-aia-*` attribute
373
387
 
374
388
  | Attribute | Element |
375
389
  |---|---|
376
- | `[data-aia-editor]` | Editor area wrapping the textarea + overlay |
377
- | `[data-aia-textarea]` | The `<textarea>` |
390
+ | `[data-aia-editor]` | Editor area wrapping the contentEditable + inline pill list |
391
+ | `[data-aia-input]` | The contentEditable `<div>` that owns typed text and bold completed params. Replaces the previous `[data-aia-textarea]` selector. |
392
+ | `[data-aia-pill-list-container]` | Inline sibling of the editor that holds unfilled-suggestion pills |
378
393
  | `[data-aia-submit]` | Submit button |
379
394
  | `[data-aia-pill]` | Each unfilled-suggestion pill |
380
395
  | `[data-aia-pillbar]` | Pill bar container inside the dropdown |
381
396
  | `[data-aia-option]` | Each suggestion option |
382
397
  | `[data-aia-dropdown]` | The dropdown root (listbox) |
383
398
 
399
+ Completed params render as inline `<strong>` elements inside the editor. Override their weight with `[data-aia-input] strong { font-weight: 700; }` (the built-in style uses `:where()` so any consumer selector wins without `!important`).
400
+
384
401
  The vanilla core also exposes stable BEM class names (`magicx-aia-*`); both can be used.
385
402
 
386
403
  ```css
package/index.d.mts CHANGED
@@ -22,8 +22,11 @@ interface Suggestion {
22
22
  interface CompletedParamState extends CompletedParam {
23
23
  id: string;
24
24
  text: string;
25
+ /** Source suggestion's `type` — used by re-edit mode to render the dropdown pill. */
25
26
  suggestionType: string;
27
+ /** Source suggestion's `text` — used by re-edit mode to render the dropdown pill. */
26
28
  suggestionPlaceholder: string;
29
+ /** Cached options from the suggestion that produced this param — re-edit shows these. */
27
30
  options: SuggestionOption[];
28
31
  metadata?: Record<string, unknown>;
29
32
  }
@@ -85,6 +88,25 @@ interface CoreState {
85
88
  skipNextFetch: boolean;
86
89
  lastRawQuery: string;
87
90
  isFocused: boolean;
91
+ /**
92
+ * Re-edit state: while non-null, the user is editing a bold completed param.
93
+ * The snapshot stays here even after the param is removed from
94
+ * `completedParams` (when typing/backspace replaces its text), so the
95
+ * dropdown can keep filtering the cached options.
96
+ */
97
+ editingParam: CompletedParamState | null;
98
+ /** Plain-text offset where the edited param started (anchor for the edit region). */
99
+ editingAnchor: number | null;
100
+ /** Plain-text offset of the right edge of the edit region. Caret outside [anchor, tail] exits edit mode. */
101
+ editingTail: number | null;
102
+ /** Current caret offset within the editor. Tracked via DOM `selectionchange`. */
103
+ caretOffset: number | null;
104
+ /**
105
+ * True for ~500ms after a user-initiated option selection so the streak
106
+ * animation can finish before the dropdown switches to its loading skeleton.
107
+ * Set by selectOption / selectOptionInEditMode, cleared by a timer.
108
+ */
109
+ inSelectionAnimation: boolean;
88
110
  }
89
111
  /**
90
112
  * Render mode:
@@ -136,6 +158,14 @@ interface CoreOptions {
136
158
  onBlur?: () => void;
137
159
  value?: string;
138
160
  completedParams?: CompletedParamState[];
161
+ /**
162
+ * Headless-mode caret setter. The core invokes this in place of writing to
163
+ * its own `domRefs.input` when re-edit / Backspace flows need to park the
164
+ * caret at a specific plain-text offset. Wrappers that own the DOM (React,
165
+ * etc.) must provide this — vanilla "full" mode falls back to `domRefs`.
166
+ * @internal
167
+ */
168
+ setCursor?: (offset: number) => void;
139
169
  }
140
170
 
141
171
  declare class AIAutocomplete {
@@ -154,6 +184,7 @@ declare class AIAutocomplete {
154
184
  private dropdownRefs;
155
185
  private newParamTimer;
156
186
  private suggestionRemovalTimer;
187
+ private selectionAnimationTimer;
157
188
  private externalListeners;
158
189
  private sessionId;
159
190
  constructor(container: HTMLElement, opts?: CoreOptions);
@@ -166,7 +197,54 @@ declare class AIAutocomplete {
166
197
  setCompletedParams(params: CompletedParamState[]): void;
167
198
  setActivePill(index: number): void;
168
199
  removeLastParam(): void;
200
+ /**
201
+ * Backspace inside a completed param: drop the param's "completed" status so
202
+ * it renders as plain (un-bold) text, AND remove one grapheme before the
203
+ * caret — same single-character delete a normal Backspace would do. The
204
+ * remaining text stays in the input so the user can continue editing what
205
+ * they had typed instead of losing the whole phrase.
206
+ *
207
+ * Returns true when a param was reconciled (caller should `preventDefault`).
208
+ */
209
+ removeParamAtCaret(offset: number): boolean;
210
+ /**
211
+ * Set the editor caret at the given plain-text offset. Uses the core's own
212
+ * `domRefs.input` in "full" mode; falls back to the wrapper-provided
213
+ * `setCursor` callback in "headless" mode where the wrapper owns the DOM.
214
+ */
215
+ private scheduleSetCursor;
169
216
  clearNewParamId(): void;
217
+ /**
218
+ * Enter re-edit mode for the bold completed param with the given id. The
219
+ * caller is responsible for highlighting the param via the Selection API
220
+ * (DOM concern). This method just sets the state snapshot so the dropdown
221
+ * starts filtering the param's cached options.
222
+ */
223
+ startEditingParam(paramId: string): void;
224
+ /**
225
+ * Replace the editing range (`text[editingAnchor..editingTail]`) with the
226
+ * given replacement. Used by the DOM layer's `beforeinput` intercept: while
227
+ * the edited param's strong still exists in `completedParams`, typing or
228
+ * backspace should swap the WHOLE param atomically (instead of inserting
229
+ * one char into the bold span). Returns true when the replacement was
230
+ * applied — callers should `preventDefault` only in that case.
231
+ */
232
+ replaceEditingRange(replacement: string): boolean;
233
+ /** Clear re-edit state. Selection collapsing is a DOM concern handled by the caller. */
234
+ exitEditMode(): void;
235
+ /**
236
+ * Update caret offset after a text-input event. While in edit mode, extends
237
+ * `editingTail` to keep up with the user typing past the previous tail.
238
+ * Exits edit mode if the user backspaced past `editingAnchor` (deleting a
239
+ * character before where the edit started).
240
+ */
241
+ handleCaretAfterInput(offset: number | null): void;
242
+ /**
243
+ * Update caret offset from a selection-move event that was NOT preceded by
244
+ * an input event (so the user moved the cursor by click or arrows, not by
245
+ * typing). Exits edit mode if the caret moved outside the edit region.
246
+ */
247
+ handleCaretMove(offset: number | null): void;
170
248
  setActiveDropdownIndex(index: number): void;
171
249
  handleTextChange(value: string): void;
172
250
  handleKeyDown(e: KeyboardEvent): void;
@@ -179,6 +257,8 @@ declare class AIAutocomplete {
179
257
  on(event: string, callback: (...args: unknown[]) => void): () => void;
180
258
  update(opts: Partial<CoreOptions>): void;
181
259
  selectOption(option: SuggestionOption): void;
260
+ private startSelectionAnimationTimer;
261
+ private selectOptionInEditMode;
182
262
  private fireTelemetry;
183
263
  private recomputeDerived;
184
264
  private setupContainer;
@@ -189,6 +269,22 @@ declare class AIAutocomplete {
189
269
  /** Auto-clear newParamId after shimmer animation. */
190
270
  private subscribeNewParamTimer;
191
271
  private handleChange;
272
+ /**
273
+ * When the user has typed text that exactly matches (case-insensitive) one
274
+ * of the active suggestion's options, promote it to a completed param right
275
+ * away. The fetchController does the same check when the debounced fetch
276
+ * lands; doing it instantly here means bold styling appears as soon as the
277
+ * option is fully typed, without waiting 100–300ms for the round-trip.
278
+ */
279
+ private maybePromoteExactMatch;
280
+ /**
281
+ * Edit-mode counterpart to {@link maybePromoteExactMatch}. When the user is
282
+ * editing a bold completed param and the typed text exactly matches one of
283
+ * that param's cached options, promote it back to a completed param (bold)
284
+ * — same instant-bold affordance as the normal typing flow, just driven by
285
+ * the edited param's cached options instead of the active suggestion.
286
+ */
287
+ private maybePromoteEditExactMatch;
192
288
  }
193
289
 
194
290
  type Listener<S> = (next: S, prev: S) => void;
package/index.d.ts CHANGED
@@ -22,8 +22,11 @@ interface Suggestion {
22
22
  interface CompletedParamState extends CompletedParam {
23
23
  id: string;
24
24
  text: string;
25
+ /** Source suggestion's `type` — used by re-edit mode to render the dropdown pill. */
25
26
  suggestionType: string;
27
+ /** Source suggestion's `text` — used by re-edit mode to render the dropdown pill. */
26
28
  suggestionPlaceholder: string;
29
+ /** Cached options from the suggestion that produced this param — re-edit shows these. */
27
30
  options: SuggestionOption[];
28
31
  metadata?: Record<string, unknown>;
29
32
  }
@@ -85,6 +88,25 @@ interface CoreState {
85
88
  skipNextFetch: boolean;
86
89
  lastRawQuery: string;
87
90
  isFocused: boolean;
91
+ /**
92
+ * Re-edit state: while non-null, the user is editing a bold completed param.
93
+ * The snapshot stays here even after the param is removed from
94
+ * `completedParams` (when typing/backspace replaces its text), so the
95
+ * dropdown can keep filtering the cached options.
96
+ */
97
+ editingParam: CompletedParamState | null;
98
+ /** Plain-text offset where the edited param started (anchor for the edit region). */
99
+ editingAnchor: number | null;
100
+ /** Plain-text offset of the right edge of the edit region. Caret outside [anchor, tail] exits edit mode. */
101
+ editingTail: number | null;
102
+ /** Current caret offset within the editor. Tracked via DOM `selectionchange`. */
103
+ caretOffset: number | null;
104
+ /**
105
+ * True for ~500ms after a user-initiated option selection so the streak
106
+ * animation can finish before the dropdown switches to its loading skeleton.
107
+ * Set by selectOption / selectOptionInEditMode, cleared by a timer.
108
+ */
109
+ inSelectionAnimation: boolean;
88
110
  }
89
111
  /**
90
112
  * Render mode:
@@ -136,6 +158,14 @@ interface CoreOptions {
136
158
  onBlur?: () => void;
137
159
  value?: string;
138
160
  completedParams?: CompletedParamState[];
161
+ /**
162
+ * Headless-mode caret setter. The core invokes this in place of writing to
163
+ * its own `domRefs.input` when re-edit / Backspace flows need to park the
164
+ * caret at a specific plain-text offset. Wrappers that own the DOM (React,
165
+ * etc.) must provide this — vanilla "full" mode falls back to `domRefs`.
166
+ * @internal
167
+ */
168
+ setCursor?: (offset: number) => void;
139
169
  }
140
170
 
141
171
  declare class AIAutocomplete {
@@ -154,6 +184,7 @@ declare class AIAutocomplete {
154
184
  private dropdownRefs;
155
185
  private newParamTimer;
156
186
  private suggestionRemovalTimer;
187
+ private selectionAnimationTimer;
157
188
  private externalListeners;
158
189
  private sessionId;
159
190
  constructor(container: HTMLElement, opts?: CoreOptions);
@@ -166,7 +197,54 @@ declare class AIAutocomplete {
166
197
  setCompletedParams(params: CompletedParamState[]): void;
167
198
  setActivePill(index: number): void;
168
199
  removeLastParam(): void;
200
+ /**
201
+ * Backspace inside a completed param: drop the param's "completed" status so
202
+ * it renders as plain (un-bold) text, AND remove one grapheme before the
203
+ * caret — same single-character delete a normal Backspace would do. The
204
+ * remaining text stays in the input so the user can continue editing what
205
+ * they had typed instead of losing the whole phrase.
206
+ *
207
+ * Returns true when a param was reconciled (caller should `preventDefault`).
208
+ */
209
+ removeParamAtCaret(offset: number): boolean;
210
+ /**
211
+ * Set the editor caret at the given plain-text offset. Uses the core's own
212
+ * `domRefs.input` in "full" mode; falls back to the wrapper-provided
213
+ * `setCursor` callback in "headless" mode where the wrapper owns the DOM.
214
+ */
215
+ private scheduleSetCursor;
169
216
  clearNewParamId(): void;
217
+ /**
218
+ * Enter re-edit mode for the bold completed param with the given id. The
219
+ * caller is responsible for highlighting the param via the Selection API
220
+ * (DOM concern). This method just sets the state snapshot so the dropdown
221
+ * starts filtering the param's cached options.
222
+ */
223
+ startEditingParam(paramId: string): void;
224
+ /**
225
+ * Replace the editing range (`text[editingAnchor..editingTail]`) with the
226
+ * given replacement. Used by the DOM layer's `beforeinput` intercept: while
227
+ * the edited param's strong still exists in `completedParams`, typing or
228
+ * backspace should swap the WHOLE param atomically (instead of inserting
229
+ * one char into the bold span). Returns true when the replacement was
230
+ * applied — callers should `preventDefault` only in that case.
231
+ */
232
+ replaceEditingRange(replacement: string): boolean;
233
+ /** Clear re-edit state. Selection collapsing is a DOM concern handled by the caller. */
234
+ exitEditMode(): void;
235
+ /**
236
+ * Update caret offset after a text-input event. While in edit mode, extends
237
+ * `editingTail` to keep up with the user typing past the previous tail.
238
+ * Exits edit mode if the user backspaced past `editingAnchor` (deleting a
239
+ * character before where the edit started).
240
+ */
241
+ handleCaretAfterInput(offset: number | null): void;
242
+ /**
243
+ * Update caret offset from a selection-move event that was NOT preceded by
244
+ * an input event (so the user moved the cursor by click or arrows, not by
245
+ * typing). Exits edit mode if the caret moved outside the edit region.
246
+ */
247
+ handleCaretMove(offset: number | null): void;
170
248
  setActiveDropdownIndex(index: number): void;
171
249
  handleTextChange(value: string): void;
172
250
  handleKeyDown(e: KeyboardEvent): void;
@@ -179,6 +257,8 @@ declare class AIAutocomplete {
179
257
  on(event: string, callback: (...args: unknown[]) => void): () => void;
180
258
  update(opts: Partial<CoreOptions>): void;
181
259
  selectOption(option: SuggestionOption): void;
260
+ private startSelectionAnimationTimer;
261
+ private selectOptionInEditMode;
182
262
  private fireTelemetry;
183
263
  private recomputeDerived;
184
264
  private setupContainer;
@@ -189,6 +269,22 @@ declare class AIAutocomplete {
189
269
  /** Auto-clear newParamId after shimmer animation. */
190
270
  private subscribeNewParamTimer;
191
271
  private handleChange;
272
+ /**
273
+ * When the user has typed text that exactly matches (case-insensitive) one
274
+ * of the active suggestion's options, promote it to a completed param right
275
+ * away. The fetchController does the same check when the debounced fetch
276
+ * lands; doing it instantly here means bold styling appears as soon as the
277
+ * option is fully typed, without waiting 100–300ms for the round-trip.
278
+ */
279
+ private maybePromoteExactMatch;
280
+ /**
281
+ * Edit-mode counterpart to {@link maybePromoteExactMatch}. When the user is
282
+ * editing a bold completed param and the typed text exactly matches one of
283
+ * that param's cached options, promote it back to a completed param (bold)
284
+ * — same instant-bold affordance as the normal typing flow, just driven by
285
+ * the edited param's cached options instead of the active suggestion.
286
+ */
287
+ private maybePromoteEditExactMatch;
192
288
  }
193
289
 
194
290
  type Listener<S> = (next: S, prev: S) => void;