@magicx-eng/ai-autocomplete-react 0.1.38 → 0.1.40

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
@@ -5,17 +5,23 @@ A React/TypeScript SDK that provides a guided AI-powered autocomplete experience
5
5
  ## Features
6
6
 
7
7
  - **Two tiers of integration** — use the full `<AIAutocomplete />` component or go headless with `useAIAutocomplete()` + `<AIAutocompleteDropdown />`
8
- - **Pill-based input** — inline pills for unfilled parameters, bold text for completed ones
8
+ - **Rich inline input (Tier 1)** — a single `contentEditable` surface: typed text, bold completed params, and inline pills share one editing context
9
+ - **Pill-based input** — non-editable inline pills for unfilled parameters, bold inline text for completed ones
10
+ - **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.
11
+ - **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
9
12
  - **Pill placement** — render pills inline in the input or inside the dropdown
10
13
  - **Light/dark mode** — built-in themes with `prefers-color-scheme` support, fully overridable via CSS variables
14
+ - **Inherits your font** — defaults to the host page's font, with `--aia-font-family` to pin a specific font on the library
11
15
  - **Access token auth** — short-lived tokens with automatic refresh, single-flight deduplication, and 401 retry
12
- - **Keyboard navigation** — arrow keys, enter to submit, tab to autocomplete
16
+ - **Keyboard navigation** — arrow keys, enter to submit, tab to autocomplete, backspace to un-bold the last completed param
17
+ - **IME-safe** — composition events are buffered so input text is committed once, after composition ends
13
18
  - **Client-side filtering** — instant substring filtering on every keystroke
14
19
  - **Option overrides** — inject or dynamically generate client-side options per suggestion type
15
20
  - **Controlled & uncontrolled** — works out of the box or integrates with external state
16
21
  - **Ref forwarding** — imperative `focus()`, `blur()`, `reset()`, and `setMode()` via ref
17
- - **Accessible** — ARIA combobox pattern with `role="listbox"`, `aria-activedescendant`
22
+ - **Accessible** — ARIA combobox 1.2 pattern with `role="listbox"`, `aria-activedescendant`
18
23
  - **Animations** — option selection streak animation, text shimmer on newly added params
24
+ - **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
25
  - **Lightweight** — styles auto-injected at runtime
20
26
  - **TypeScript first** — full type definitions shipped with the package
21
27
 
@@ -96,7 +102,7 @@ const ref = useRef<AIAutocompleteHandle>(null);
96
102
 
97
103
  ### Focus Control
98
104
 
99
- The component auto-focuses the input on mount. To opt out, pass `autoFocus={false}`. Listen to focus changes with `onFocus` / `onBlur`:
105
+ The component auto-focuses the contentEditable editor on mount. To opt out, pass `autoFocus={false}`. Listen to focus changes with `onFocus` / `onBlur`:
100
106
 
101
107
  ```tsx
102
108
  <AIAutocomplete
@@ -107,6 +113,20 @@ The component auto-focuses the input on mount. To opt out, pass `autoFocus={fals
107
113
  />
108
114
  ```
109
115
 
116
+ ### Backspace into a completed param
117
+
118
+ 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.
119
+
120
+ ### Re-edit a completed param
121
+
122
+ Tapping a bold completed param enters *re-edit mode* — the dropdown re-opens with the cached options the server originally returned for that param. From there:
123
+
124
+ - **Typing** atomically replaces the bold with what you type. If what you type exactly matches one of the cached options, it's re-promoted to a bold completed param immediately.
125
+ - **Clicking an option** replaces the bold with the new selection.
126
+ - **Arrow keys, Escape, or clicking outside the param** exit re-edit mode without changing anything.
127
+
128
+ After a completed param is added (by any means — option click, exact-match typing, or re-edit), the caret always lands right after the trailing space following the bold so typing can continue immediately. A space is inserted if one wasn't already there.
129
+
110
130
  ---
111
131
 
112
132
  ## Tier 2: Headless
@@ -246,11 +266,11 @@ The headless hook for Tier 2. Accepts the same props as `<AIAutocomplete />` exc
246
266
  |---|---|---|
247
267
  | `completedParams` | `CompletedParamState[]` | Filled parameters. |
248
268
  | `suggestionPills` | `Suggestion[]` | Unfilled suggestions (pills). First item is the active pill. |
249
- | `segments` | `Segment[]` | Input text split into text vs completed segments for overlay rendering. |
269
+ | `segments` | `Segment[]` | Input text split into typed text vs completed params — completed segments render as bold `<strong>` runs inside the editor. |
250
270
  | `newParamId` | `string \| null` | ID of the most recently added param (for shimmer animation). |
251
271
  | `suggestions` | `Suggestion[]` | All suggestions from server (including placeholder type). |
252
272
  | `activeIndex` | `number` | Highlighted option index. `-1` = none. |
253
- | `isLoading` | `boolean` | Fetch in progress. |
273
+ | `isLoading` | `boolean` | True when the dropdown should render its loading skeleton — a fetch is in flight, no option-selection animation is playing, and the user is not in re-edit mode (where cached options stay visible). |
254
274
  | `isReady` | `boolean` | Server indicates query is complete. |
255
275
  | `error` | `Error \| null` | Last fetch error. |
256
276
 
@@ -259,7 +279,7 @@ The headless hook for Tier 2. Accepts the same props as `<AIAutocomplete />` exc
259
279
  | Field | Type | Description |
260
280
  |---|---|---|
261
281
  | `setActivePill` | `(index: number) => void` | Move pill at `index` to front (active). |
262
- | `removeLastParam` | `() => void` | Remove last completed param, restore as pill. |
282
+ | `removeLastParam` | `() => void` | Remove the last completed param from state. The text stays in the input as plain text. |
263
283
  | `clearNewParamId` | `() => void` | Clear shimmer animation state. |
264
284
  | `reset` | `() => void` | Clear all state, re-fetch, and start a new session (rotates `session_id`). Call this after handling submit. |
265
285
 
@@ -286,6 +306,7 @@ The dropdown component for Tier 2. Spread `dropdownProps` from the hook.
286
306
  | `pills?` | `Suggestion[]` | Pills to render inside the dropdown. |
287
307
  | `onPillClick?` | `(index: number) => void` | Called when a pill is clicked. |
288
308
  | `showPills?` | `boolean` | Whether to render pills. Default: `true`. |
309
+ | `isLoading?` | `boolean` | When `true`, the dropdown renders its pills + options as a skeleton: text masked, layout (count and widths) preserved, shimmer pulse animating. Falls back to a generic 3-bar placeholder when no pills/options are cached. |
289
310
 
290
311
  ### `AutocompleteResult`
291
312
 
@@ -307,6 +328,7 @@ Override on the container (via `className`). All defaults use `:where()` (zero s
307
328
 
308
329
  | Variable | Light | Dark | Description |
309
330
  |---|---|---|---|
331
+ | `--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. |
310
332
  | `--aia-pill-bg` | `#bdbdbd` | `#bdbdbd` | Pill background |
311
333
  | `--aia-pill-color` | `#000000` | `#ffffff` | Pill text |
312
334
  | `--aia-pill-font-size` | `19px` | `19px` | Pill font size |
@@ -316,13 +338,14 @@ Override on the container (via `className`). All defaults use `:where()` (zero s
316
338
  | `--aia-option-font-size` | `19px` | `19px` | Option font size |
317
339
  | `--aia-written-text-color` | `#000000` | `#ffffff` | Input text |
318
340
  | `--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. |
341
+ | `--aia-caret-color` | `--aia-written-text-color` | `--aia-written-text-color` | Editor caret color. Override independently of input text color. |
320
342
  | `--aia-submit-bg` | `#000000` | `#ffffff` | Submit button background |
321
343
  | `--aia-submit-color` | `#ffffff` | `#000000` | Submit button icon color |
322
344
  | `--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
345
  | `--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
346
  | `--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
347
  | `--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. |
348
+ | `--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. |
326
349
 
327
350
  Legacy `--aia-color-*` variables are still supported as fallbacks.
328
351
 
@@ -343,14 +366,17 @@ For styling beyond the CSS variables, target these stable `data-aia-*` attribute
343
366
 
344
367
  | Attribute | Element |
345
368
  |---|---|
346
- | `[data-aia-editor]` | Editor area wrapping the textarea + overlay |
347
- | `[data-aia-textarea]` | The `<textarea>` |
369
+ | `[data-aia-editor]` | Editor area wrapping the contentEditable + inline pill list |
370
+ | `[data-aia-input]` | The Tier 1 contentEditable `<div>` that owns typed text and bold completed params. (Replaces the previous `[data-aia-textarea]` selector.) |
371
+ | `[data-aia-pill-list-container]` | Inline sibling of the editor that holds unfilled-suggestion pills |
348
372
  | `[data-aia-submit]` | Submit button |
349
373
  | `[data-aia-pill]` | Each unfilled-suggestion pill |
350
374
  | `[data-aia-pillbar]` | Pill bar container inside the dropdown |
351
375
  | `[data-aia-option]` | Each suggestion option |
352
376
  | `[data-aia-dropdown]` | The dropdown root (listbox) |
353
377
 
378
+ 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`).
379
+
354
380
  ```css
355
381
  /* Solid (non-glass) dropdown */
356
382
  .my-autocomplete [data-aia-dropdown] {
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ChangeEvent, KeyboardEvent } from 'react';
2
+ import { ReactNode, KeyboardEvent, ChangeEvent } from 'react';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
 
5
5
  type TaskKind = "automation" | "email" | "insight";
@@ -26,8 +26,11 @@ interface Suggestion {
26
26
  interface CompletedParamState extends CompletedParam {
27
27
  id: string;
28
28
  text: string;
29
+ /** Source suggestion's `type` — used by re-edit mode to render the dropdown pill. */
29
30
  suggestionType: string;
31
+ /** Source suggestion's `text` — used by re-edit mode to render the dropdown pill. */
30
32
  suggestionPlaceholder: string;
33
+ /** Cached options from the suggestion that produced this param — re-edit shows these. */
31
34
  options: SuggestionOption[];
32
35
  metadata?: Record<string, unknown>;
33
36
  }
@@ -136,6 +139,14 @@ interface UseAIAutocompleteOptions {
136
139
  completedParams?: CompletedParamState[];
137
140
  onChange?: (value: string) => void;
138
141
  onParamsChange?: (params: CompletedParamState[]) => void;
142
+ /**
143
+ * Tier 1 helper. Invoked by the core when re-edit / Backspace flows need
144
+ * the editor caret parked at a specific plain-text offset. The Tier 1
145
+ * `<AIAutocomplete />` component supplies this from its inputRef; headless
146
+ * consumers can ignore it.
147
+ * @internal
148
+ */
149
+ setCursor?: (offset: number) => void;
139
150
  }
140
151
  interface UseAIAutocompleteReturn {
141
152
  completedParams: CompletedParamState[];
@@ -150,7 +161,33 @@ interface UseAIAutocompleteReturn {
150
161
  activeIndex: number;
151
162
  isReady: boolean;
152
163
  isLoading: boolean;
164
+ isFocused: boolean;
165
+ isDropdownOpen: boolean;
166
+ placeholderText: string;
167
+ listboxId: string;
153
168
  error: Error | null;
169
+ /** Tier 1 helper: forward plain-text input to the core (autocapitalize handled). */
170
+ handleTextChange: (value: string) => void;
171
+ /** Tier 1 helper: forward keyboard events to the core. */
172
+ handleKeyDown: (e: KeyboardEvent<HTMLElement> | globalThis.KeyboardEvent) => void;
173
+ /** Tier 1 helper: notify the core of focus state. */
174
+ setFocused: (focused: boolean) => void;
175
+ /** Tier 1 re-edit: snapshot of the bold param currently being re-edited (null when not editing). */
176
+ editingParam: CompletedParamState | null;
177
+ /** Tier 1 re-edit: plain-text offset where the editing region starts (null when not editing). */
178
+ editingAnchor: number | null;
179
+ /** Tier 1 helper: latest known caret offset within the editor. Promote paths stamp this with the position the caret should land after a completion. */
180
+ caretOffset: number | null;
181
+ /** Tier 1 re-edit: enter re-edit mode for the bold param with the given id. */
182
+ startEditingParam: (paramId: string) => void;
183
+ /** Tier 1 re-edit: exit re-edit mode. */
184
+ exitEditMode: () => void;
185
+ /** Tier 1 re-edit: update caret tracking after a text-input event (extends edit tail). */
186
+ handleCaretAfterInput: (offset: number | null) => void;
187
+ /** Tier 1 re-edit: update caret on a selection-only move (may exit edit mode). */
188
+ handleCaretMove: (offset: number | null) => void;
189
+ /** Tier 1 re-edit: replace the bold param being edited with the given text. Returns true when applied. */
190
+ replaceEditingRange: (replacement: string) => boolean;
154
191
  inputProps: {
155
192
  value: string;
156
193
  placeholder: string | undefined;
@@ -180,12 +217,14 @@ interface AIAutocompleteDropdownProps {
180
217
  onPillClick?: (index: number) => void;
181
218
  /** Whether to render pills inside the dropdown. Default: true. Tier 2 consumers who render their own pills should set this to false. */
182
219
  showPills?: boolean;
220
+ /** True while a fetch for the latest query is in flight. Replaces options/pills with a skeleton. */
221
+ isLoading?: boolean;
183
222
  }
184
223
 
185
224
  declare const AIAutocomplete: react.ForwardRefExoticComponent<AIAutocompleteProps & react.RefAttributes<AIAutocompleteHandle>>;
186
225
 
187
- declare function AIAutocompleteDropdown({ suggestions, activeIndex, onSelect, onHighlight, isOpen, id, className, pills, onPillClick, showPills, }: AIAutocompleteDropdownProps): react_jsx_runtime.JSX.Element;
226
+ declare function AIAutocompleteDropdown({ suggestions, activeIndex, onSelect, onHighlight, isOpen, id, className, pills, onPillClick, showPills, isLoading, }: AIAutocompleteDropdownProps): react_jsx_runtime.JSX.Element;
188
227
 
189
- declare function useAIAutocomplete({ onSubmit, onError, optionOverrides, maskCompletedText, apiConfig, columns, dropdownTrigger, closeDropdownOnBlur, onFocus, onBlur, value: controlledValue, completedParams: controlledParams, onChange: onChangeProp, onParamsChange, source, }: UseAIAutocompleteOptions): UseAIAutocompleteReturn;
228
+ declare function useAIAutocomplete({ onSubmit, onError, optionOverrides, maskCompletedText, apiConfig, columns, dropdownTrigger, closeDropdownOnBlur, onFocus, onBlur, value: controlledValue, completedParams: controlledParams, onChange: onChangeProp, onParamsChange, source, setCursor, }: UseAIAutocompleteOptions): UseAIAutocompleteReturn;
190
229
 
191
230
  export { AIAutocomplete, AIAutocompleteDropdown, type AIAutocompleteDropdownProps, type AIAutocompleteHandle, type AIAutocompleteProps, type APIConfig, type APIKeyConfig, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteResult, type CompletedParam, type CompletedParamState, type OptionOverrides, type Segment, type Suggestion, type SuggestionOption, type TaskKind, type UseAIAutocompleteOptions, type UseAIAutocompleteReturn, useAIAutocomplete };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ChangeEvent, KeyboardEvent } from 'react';
2
+ import { ReactNode, KeyboardEvent, ChangeEvent } from 'react';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
 
5
5
  type TaskKind = "automation" | "email" | "insight";
@@ -26,8 +26,11 @@ interface Suggestion {
26
26
  interface CompletedParamState extends CompletedParam {
27
27
  id: string;
28
28
  text: string;
29
+ /** Source suggestion's `type` — used by re-edit mode to render the dropdown pill. */
29
30
  suggestionType: string;
31
+ /** Source suggestion's `text` — used by re-edit mode to render the dropdown pill. */
30
32
  suggestionPlaceholder: string;
33
+ /** Cached options from the suggestion that produced this param — re-edit shows these. */
31
34
  options: SuggestionOption[];
32
35
  metadata?: Record<string, unknown>;
33
36
  }
@@ -136,6 +139,14 @@ interface UseAIAutocompleteOptions {
136
139
  completedParams?: CompletedParamState[];
137
140
  onChange?: (value: string) => void;
138
141
  onParamsChange?: (params: CompletedParamState[]) => void;
142
+ /**
143
+ * Tier 1 helper. Invoked by the core when re-edit / Backspace flows need
144
+ * the editor caret parked at a specific plain-text offset. The Tier 1
145
+ * `<AIAutocomplete />` component supplies this from its inputRef; headless
146
+ * consumers can ignore it.
147
+ * @internal
148
+ */
149
+ setCursor?: (offset: number) => void;
139
150
  }
140
151
  interface UseAIAutocompleteReturn {
141
152
  completedParams: CompletedParamState[];
@@ -150,7 +161,33 @@ interface UseAIAutocompleteReturn {
150
161
  activeIndex: number;
151
162
  isReady: boolean;
152
163
  isLoading: boolean;
164
+ isFocused: boolean;
165
+ isDropdownOpen: boolean;
166
+ placeholderText: string;
167
+ listboxId: string;
153
168
  error: Error | null;
169
+ /** Tier 1 helper: forward plain-text input to the core (autocapitalize handled). */
170
+ handleTextChange: (value: string) => void;
171
+ /** Tier 1 helper: forward keyboard events to the core. */
172
+ handleKeyDown: (e: KeyboardEvent<HTMLElement> | globalThis.KeyboardEvent) => void;
173
+ /** Tier 1 helper: notify the core of focus state. */
174
+ setFocused: (focused: boolean) => void;
175
+ /** Tier 1 re-edit: snapshot of the bold param currently being re-edited (null when not editing). */
176
+ editingParam: CompletedParamState | null;
177
+ /** Tier 1 re-edit: plain-text offset where the editing region starts (null when not editing). */
178
+ editingAnchor: number | null;
179
+ /** Tier 1 helper: latest known caret offset within the editor. Promote paths stamp this with the position the caret should land after a completion. */
180
+ caretOffset: number | null;
181
+ /** Tier 1 re-edit: enter re-edit mode for the bold param with the given id. */
182
+ startEditingParam: (paramId: string) => void;
183
+ /** Tier 1 re-edit: exit re-edit mode. */
184
+ exitEditMode: () => void;
185
+ /** Tier 1 re-edit: update caret tracking after a text-input event (extends edit tail). */
186
+ handleCaretAfterInput: (offset: number | null) => void;
187
+ /** Tier 1 re-edit: update caret on a selection-only move (may exit edit mode). */
188
+ handleCaretMove: (offset: number | null) => void;
189
+ /** Tier 1 re-edit: replace the bold param being edited with the given text. Returns true when applied. */
190
+ replaceEditingRange: (replacement: string) => boolean;
154
191
  inputProps: {
155
192
  value: string;
156
193
  placeholder: string | undefined;
@@ -180,12 +217,14 @@ interface AIAutocompleteDropdownProps {
180
217
  onPillClick?: (index: number) => void;
181
218
  /** Whether to render pills inside the dropdown. Default: true. Tier 2 consumers who render their own pills should set this to false. */
182
219
  showPills?: boolean;
220
+ /** True while a fetch for the latest query is in flight. Replaces options/pills with a skeleton. */
221
+ isLoading?: boolean;
183
222
  }
184
223
 
185
224
  declare const AIAutocomplete: react.ForwardRefExoticComponent<AIAutocompleteProps & react.RefAttributes<AIAutocompleteHandle>>;
186
225
 
187
- declare function AIAutocompleteDropdown({ suggestions, activeIndex, onSelect, onHighlight, isOpen, id, className, pills, onPillClick, showPills, }: AIAutocompleteDropdownProps): react_jsx_runtime.JSX.Element;
226
+ declare function AIAutocompleteDropdown({ suggestions, activeIndex, onSelect, onHighlight, isOpen, id, className, pills, onPillClick, showPills, isLoading, }: AIAutocompleteDropdownProps): react_jsx_runtime.JSX.Element;
188
227
 
189
- declare function useAIAutocomplete({ onSubmit, onError, optionOverrides, maskCompletedText, apiConfig, columns, dropdownTrigger, closeDropdownOnBlur, onFocus, onBlur, value: controlledValue, completedParams: controlledParams, onChange: onChangeProp, onParamsChange, source, }: UseAIAutocompleteOptions): UseAIAutocompleteReturn;
228
+ declare function useAIAutocomplete({ onSubmit, onError, optionOverrides, maskCompletedText, apiConfig, columns, dropdownTrigger, closeDropdownOnBlur, onFocus, onBlur, value: controlledValue, completedParams: controlledParams, onChange: onChangeProp, onParamsChange, source, setCursor, }: UseAIAutocompleteOptions): UseAIAutocompleteReturn;
190
229
 
191
230
  export { AIAutocomplete, AIAutocompleteDropdown, type AIAutocompleteDropdownProps, type AIAutocompleteHandle, type AIAutocompleteProps, type APIConfig, type APIKeyConfig, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteResult, type CompletedParam, type CompletedParamState, type OptionOverrides, type Segment, type Suggestion, type SuggestionOption, type TaskKind, type UseAIAutocompleteOptions, type UseAIAutocompleteReturn, useAIAutocomplete };