@algolia/wizard 0.44.0 → 0.45.0-rc.141.320

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/dist/main.js CHANGED
@@ -3472,7 +3472,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3472
3472
  // package.json
3473
3473
  var package_default = {
3474
3474
  name: "@algolia/wizard",
3475
- version: "0.44.0",
3475
+ version: "0.45.0-rc.141.320",
3476
3476
  description: "Magically implement Algolia functionality in your codebase",
3477
3477
  type: "module",
3478
3478
  engines: {
@@ -4184,6 +4184,7 @@ function searchInstructions(input) {
4184
4184
  return [
4185
4185
  "Implement an in-app Algolia search experience.",
4186
4186
  `Build the search UI for ${input.searchUiTarget}.`,
4187
+ "Search UI only \u2014 the index is already populated. Do not create or modify ingestion scripts, rake/manage/CLI tasks, migrations, seeders, or other data-loading code, even if the data looks incomplete.",
4187
4188
  ...doc ? [
4188
4189
  "Follow the Algolia SDK reference below for client setup, search UI wiring, and Insights instrumentation \u2014 Insights is required, not optional; prefer the reference over prior knowledge:",
4189
4190
  doc
@@ -4191,9 +4192,15 @@ function searchInstructions(input) {
4191
4192
  "No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
4192
4193
  ],
4193
4194
  `Create the search experience as its own component in a new file, following the project's existing component conventions (location, naming, styling approach). Do not write it inline into an existing file.`,
4194
- `Import and render that new component from ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results list against the target index.`,
4195
+ `Import and render that new component from ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results panel against the target index.`,
4195
4196
  "If a search box already exists, replace its usage with an import and render of your new component; remove the old implementation.",
4196
4197
  "When rendering results with an existing shared component (e.g. a card), import and reuse that component rather than inlining its markup \u2014 inlining silently drops the styles and behavior its own file provides.",
4198
+ "Results go in a dropdown panel, not a permanently visible list. If the SDK reference above documents an Autocomplete widget for this stack, it already owns panel open/close, Escape, blur, outside click, arrow-key navigation, combobox ARIA, and Insights events \u2014 use it as-is and add no open/close logic, query gating, or ARIA roles of your own, even if it opens on focus before anything is typed. Build it by hand only where the reference has no such widget or calls it headless: then open the panel only while the input is focused with a non-empty query, close it on Escape, blur, and outside click, and move an active-result highlight with Up/Down, Enter to select.",
4199
+ "Let the widget own selection: where hits have a detail page, pass its per-index URL option (getURL or the flavor's equivalent) so it navigates on click and on Enter with a result highlighted, and keep your item template or component to content only \u2014 no anchor, no href, no click handler of your own, no stopPropagation. The widget's result element sends the Insights click event, closes the panel, and clears the active option; your own link swallows all three while still navigating, so it looks correct and silently loses click analytics.",
4200
+ "Render the zero-results state through the widget's own no-results template or component, so a query matching nothing shows a message rather than an empty panel.",
4201
+ "Meet WCAG AA contrast (4.5:1 body text, 3:1 large text/icons) between the panel's text and its own background, and give the input and the active result a focus indicator visible against whatever sits behind it. Style the panel through the widget's class and CSS-variable overrides (or, when hand-built, the app's existing theme tokens) \u2014 never assume a light surface or reuse the surrounding page's colors unchanged inside the panel.",
4202
+ "Vendor theme CSS, and any CSS you write against vendor class names, must be global: import the theme from the component's own script module or the app's global stylesheet, and put overrides in a global block (Astro <style is:global>, an unscoped Vue block, a plain global CSS file). Never a framework-scoped style block or a CSS Module \u2014 scoping rewrites the vendor selectors and the widget's runtime DOM carries no scope attribute, so not one rule matches: styling silently does nothing and the build still passes.",
4203
+ "The panel takes its width from the input by default, so a small input makes it unreadably narrow: give it a min-width of 320px independent of the input, anchored to the input edge it opens from so widening does not push it off-screen.",
4197
4204
  `Define ${SEARCH_CONFIG_APP_ID}, ${SEARCH_CONFIG_SEARCH_KEY}, and ${SEARCH_CONFIG_INDEX_NAME} as exported constants in a module that fits this project's existing conventions for shared client-side config \u2014 reuse an existing one if it already holds config like this, or add a small new one otherwise. These are PUBLIC values, safe to commit and expose client-side: never read them from an environment variable or a .env* file, and never hardcode them anywhere except in that one module (import them wherever the search client needs them).`,
4198
4205
  `Set ${SEARCH_CONFIG_APP_ID} to "${input.appId}" and ${SEARCH_CONFIG_INDEX_NAME} to "${input.targetIndex}".`,
4199
4206
  input.searchKey ? `Set ${SEARCH_CONFIG_SEARCH_KEY} to "${input.searchKey}".` : `A real search-only key could not be provisioned${input.searchKeyError ? ` (${input.searchKeyError})` : ""} \u2014 set ${SEARCH_CONFIG_SEARCH_KEY} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a real one.`,
@@ -20,14 +20,20 @@ Always install the **latest stable** within the major (use a caret range like `^
20
20
 
21
21
  - Use a **search-only API key** in any browser/client code. It is safe to expose.
22
22
  NEVER ship an admin or any write-capable key to the client.
23
- - Read the Application ID and search-only key from **public env vars**, never hardcode
24
- them inline.
23
+ - Import the Application ID and search-only key from the project's shared client-side
24
+ config module as plain exported constants. They are public, safe to commit — never read
25
+ them from an env var or `.env*` file, and never hardcode them elsewhere.
25
26
  - Instantiate the search client **once, outside your components**, and pass a stable
26
27
  reference. Do not inline `algoliasearch(...)` as a prop value — it breaks the
27
28
  client cache and causes re-renders.
28
29
  - For InstantSearch, import the client from `algoliasearch/lite` (smaller bundle and
29
30
  correct types — see `instantsearch-setup.md`).
30
31
  - Always enable **Insights** on the InstantSearch root with `insights: true`. Without it there are no click/conversion events, so many Algolia features will not work as expected.
32
+ - Vendor CSS must be **global**: import an InstantSearch theme from a JS/TS module or the
33
+ app's global stylesheet and keep `.ais-*` overrides in a global block. In a scoped style
34
+ block (Astro `<style>`, Vue `<style scoped>`, Svelte `<style>`) or a CSS Module, scoping
35
+ rewrites the selectors, the widget's runtime DOM carries no scope attribute, and the
36
+ styling silently does nothing while the build passes.
31
37
 
32
38
  ## Files
33
39
 
@@ -20,10 +20,18 @@ const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
20
20
 
21
21
  ## Vanilla (`instantsearch.js` v4)
22
22
 
23
+ Use the `autocomplete` widget for the results panel. It owns focus, blur, outside-click
24
+ dismissal, Up/Down navigation with wrap-around, Enter/Escape/Tab, and the combobox ARIA
25
+ (`combobox`, `aria-expanded`, `aria-activedescendant`) — add no open/close logic or ARIA
26
+ roles of your own.
27
+
28
+ Needs `instantsearch.js` **4.110.0+**. If an older 4.x is pinned, upgrade within `^4`
29
+ rather than switching widgets.
30
+
23
31
  ```js
24
32
  import { liteClient as algoliasearch } from 'algoliasearch/lite'
25
33
  import instantsearch from 'instantsearch.js'
26
- import { searchBox, hits } from 'instantsearch.js/es/widgets'
34
+ import { autocomplete } from 'instantsearch.js/es/widgets'
27
35
  import {
28
36
  ALGOLIA_APP_ID,
29
37
  ALGOLIA_SEARCH_API_KEY,
@@ -39,24 +47,140 @@ const search = instantsearch({
39
47
  })
40
48
 
41
49
  search.addWidgets([
42
- searchBox({ container: '#searchbox' }),
43
- hits({ container: '#hits' }),
50
+ autocomplete({
51
+ container: '#autocomplete',
52
+ placeholder: 'Search...',
53
+ indices: [
54
+ {
55
+ indexName: ALGOLIA_INDEX_NAME,
56
+ getURL: (item) => `/products/${item.handle}`,
57
+ templates: {
58
+ item({ item, onSelect }, { html, components }) {
59
+ return html`<div onClick=${onSelect}>
60
+ ${components.Highlight({ hit: item, attribute: 'name' })}
61
+ </div>`
62
+ },
63
+ noResults(_, { html }) {
64
+ return html`<span>No results</span>`
65
+ },
66
+ },
67
+ },
68
+ ],
69
+ }),
44
70
  ])
45
71
 
46
72
  search.start()
47
73
  ```
48
74
 
75
+ ### Navigation is `getURL`'s job, not the template's
76
+
77
+ - `indices[].getURL` — `(item) => string`, a route that already exists; don't invent one.
78
+ It navigates on click **and** on Enter over a highlighted item, and records the click
79
+ event. Without it, Enter on a highlight does nothing.
80
+ - The `item` template renders content only: **no `<a>`, no `href`, no click handler of
81
+ your own, never `stopPropagation`.** The widget's `<li>` owns the click — it sends the
82
+ Insights `click`, closes the panel, clears the active option. An anchor wrapper swallows
83
+ all three while still navigating: looks correct, click analytics never fire, panel stays
84
+ open behind the new page.
85
+ - `templates.noResults` on every index, or a query matching nothing renders an empty box.
86
+ - `getSearchPageURL` sends Enter with nothing highlighted to a full results page; pass it
87
+ only if the app has one. `.getQuery` is optional. Leave `showQuerySuggestions`,
88
+ `showPromptSuggestions`, `showRecent`, and `aiMode` unset — each needs an index or
89
+ feature (query suggestions, recent-search storage, AI chat) this project lacks.
90
+ - TypeScript: type records through the generic, not casts inside templates —
91
+ `type ProductHit = Hit<{ title: string; handle: string }>` (`Hit` from
92
+ `instantsearch.js`), then `autocomplete<ProductHit>({ ... })` types `item`, `getURL`,
93
+ and the templates.
94
+
95
+ ### Styling the panel
96
+
97
+ The widget ships no CSS. Install `instantsearch.css` and import a theme once, from the
98
+ component's own JS/TS module or the app's global stylesheet:
99
+
100
+ ```js
101
+ import 'instantsearch.css/themes/satellite.css'
102
+ ```
103
+
104
+ The theme and your `.ais-*` overrides must be **global**: never a CSS `@import` inside a
105
+ framework-scoped style block (Astro `<style>`, Vue `<style scoped>`, Svelte `<style>`) or
106
+ a CSS Module. Scoping rewrites every selector
107
+ (`.ais-AutocompleteInput[data-astro-cid-abc123]`) and the widget builds its DOM at runtime
108
+ with no scope attribute, so not one rule matches — unstyled UI, no error, build passes.
109
+ Use `<style is:global>` (Astro), an unscoped block or `:deep()` (Vue), or plain global CSS.
110
+
111
+ The theme ships a dark palette behind `:root[data-theme=dark], .dark`: in a dark app set
112
+ `data-theme="dark"` on `<html>` or render under a `.dark` ancestor, then adjust — don't
113
+ redefine the light palette by hand.
114
+
115
+ Palette lives in `--ais-*` custom properties on `:root` as **RGB triplets consumed through
116
+ `rgba()`**, not CSS colors — override with triplets or every widget color breaks:
117
+
118
+ ```css
119
+ :root {
120
+ --ais-background-color-rgb: 24, 24, 27; /* not #18181b */
121
+ --ais-text-color-rgb: 244, 244, 245;
122
+ --ais-primary-color-rgb: 129, 140, 248;
123
+ }
124
+ ```
125
+
126
+ Everything else is plain CSS against the widget's classes, or `cssClasses` /
127
+ `indices[].cssClasses` to add your own names to the same elements. Who owns what:
128
+
129
+ - `.ais-Autocomplete` — root. Its **container** sets the widget's width; don't size the
130
+ widget from the input.
131
+ - `.ais-AutocompleteForm` — visible frame: background, border, radius, focus ring
132
+ (`:focus-within`). The input's pill goes here.
133
+ - `.ais-AutocompleteInput` — text only; theme sets `border: 0`, `background: none`,
134
+ `width: 100%`. A border or width here leaves the form's frame drawn around it and
135
+ changes no widths.
136
+ - `.ais-AutocompletePanel` (+ `--open`), `.ais-AutocompletePanelLayout` — dropdown surface
137
+ and its scroll container.
138
+ - `.ais-AutocompleteIndex`, `-IndexHeader`, `-IndexList`, `-IndexItem`, `-IndexNoResults`
139
+ — one index's results; the active item carries `aria-selected="true"`.
140
+ - `.ais-AutocompleteItemWrapper`, `-ItemContent`, `-ItemIcon` — inside one result.
141
+ - `.ais-AutocompleteSubmitButton`, `-ClearButton`, `-LoadingIndicator`, `-Label` — form
142
+ controls.
143
+ - `.ais-AutocompleteDetached*` — detached mode (below).
144
+
145
+ Style nothing outside that list: `.ais-SearchBox-*` belongs to `searchBox` and does
146
+ nothing here. Verify names against the installed version:
147
+ `grep -o 'ais-Autocomplete[A-Za-z-]*' node_modules/instantsearch.css/themes/satellite.css | sort -u`.
148
+
149
+ The theme's panel is `position: absolute; width: 100%`, so it inherits the input
150
+ container's width — a small input yields an unreadably narrow panel. Give it a min-width
151
+ and anchor it to the edge it opens from, or it grows off-screen:
152
+
153
+ ```css
154
+ .ais-AutocompletePanel {
155
+ min-width: 320px;
156
+ left: 0; /* right: 0; left: auto for an input aligned to the right */
157
+ }
158
+ ```
159
+
160
+ Below `(max-width: 680px)` the widget goes detached (fullscreen): the inline form becomes
161
+ a `.ais-AutocompleteDetachedSearchButton`, form and results move into an overlay
162
+ (`.ais-AutocompleteDetachedOverlay`, `.ais-AutocompleteDetachedContainer`,
163
+ `.ais-AutocompleteDetachedFormContainer`). So panel-width rules apply only above that
164
+ breakpoint, and styling the inline form leaves the mobile trigger button on the theme's
165
+ look unless you style it too. Change it with `detachedMediaQuery` or
166
+ `--ais-autocomplete-detached-media-query`; pass `''` to disable detached mode entirely.
167
+
49
168
  ## Insights (click analytics) — always enable it
50
169
 
51
170
  The `insights` option turns on the Insights middleware (`instantsearch.js` v4.55+),
52
171
  which adds `clickAnalytics: true` to every query and keeps the `userToken` in sync
53
172
  between search and event calls. Never set `clickAnalytics` or `userToken` on the `configure` widget by hand.
54
173
 
55
- Use `insights: true`. The middleware loads `search-insights` from the jsDelivr CDN
56
- itself; do not install the package or pass `insightsClient`.
174
+ Use `insights: true` and nothing else. The middleware loads `search-insights` from the
175
+ jsDelivr CDN itself; do not install the package, pass `insightsClient`, or declare
176
+ `window.aa`.
57
177
 
58
- If a Content-Security-Policy blocks jsDelivr, the CDN script cannot load. In that
59
- case, install `search-insights` from npm and pass its default export as
178
+ Everything below is a **conditional fallback** — apply one only where its condition
179
+ actually holds here (a CSP you can point at, a user id the app already has). Otherwise
180
+ `insights: true` is the whole implementation.
181
+
182
+ Only if the project has a Content-Security-Policy blocking jsDelivr (the CDN script then
183
+ cannot load): install `search-insights` from npm and pass its default export as
60
184
  `insightsClient` in the `insights` option:
61
185
 
62
186
  ```js
@@ -90,27 +214,16 @@ insights: {
90
214
 
91
215
  ### Click and conversion events
92
216
 
93
- The `hits` and `infiniteHits` widgets send `view` and `click` events on their own. A
94
- custom `item` template whose click target is your own element gets `sendEvent` as the
95
- second argument:
217
+ With `insights: true`, `autocomplete` sends `view` (on render) and `click` (from the
218
+ result element's own handler) itself. Its item template gets `{ item, onSelect }` and
219
+ **no `sendEvent`** — nothing to wire. Call `onSelect` for the widget's selection behavior
220
+ (close the panel, record a recent search); the click event fires either way.
96
221
 
97
- ```js
98
- hits({
99
- container: '#hits',
100
- templates: {
101
- item(hit, { html, sendEvent }) {
102
- return html`<a
103
- href="${hit.url}"
104
- onClick="${() => sendEvent('click', hit, 'Hit Clicked')}"
105
- >${hit.name}</a
106
- >`
107
- },
108
- },
109
- })
110
- ```
222
+ `hits` and `infiniteHits` behave the same; only a custom `item` template whose click
223
+ target is your own element needs wiring — it gets `sendEvent` as the second argument.
111
224
 
112
- Conversions are business actions (add to cart, signup, booking), so wire them at the
113
- real action, not at render:
225
+ Conversions are business actions (add to cart, signup, booking) away from the results
226
+ panel: wire them at the real action, in a component with its own `sendEvent`:
114
227
 
115
228
  ```js
116
229
  sendEvent('conversion', hit, 'Product Added To Cart', {
@@ -20,9 +20,17 @@ const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
20
20
 
21
21
  ## React (`react-instantsearch` v7)
22
22
 
23
+ Use the `Autocomplete` widget for the results panel. It owns focus, blur, outside-click
24
+ dismissal, Up/Down navigation with wrap-around, Enter/Escape/Tab, and the combobox ARIA
25
+ (`combobox` on the input, `aria-expanded`, `aria-activedescendant`) — add no open/close
26
+ logic or ARIA roles of your own.
27
+
28
+ Needs `react-instantsearch` **7.18.0+**. If an older 7.x is pinned, upgrade within `^7`
29
+ rather than switching widgets.
30
+
23
31
  ```tsx
24
32
  import { liteClient as algoliasearch } from 'algoliasearch/lite'
25
- import { InstantSearch, SearchBox, Hits } from 'react-instantsearch'
33
+ import { InstantSearch, Autocomplete } from 'react-instantsearch'
26
34
  import {
27
35
  ALGOLIA_APP_ID,
28
36
  ALGOLIA_SEARCH_API_KEY,
@@ -38,13 +46,117 @@ export function Search() {
38
46
  indexName={ALGOLIA_INDEX_NAME}
39
47
  insights={true}
40
48
  >
41
- <SearchBox />
42
- <Hits />
49
+ <Autocomplete
50
+ placeholder="Search..."
51
+ indices={[
52
+ {
53
+ indexName: ALGOLIA_INDEX_NAME,
54
+ getURL: (item) => `/products/${item.handle}`,
55
+ itemComponent: ({ item, onSelect }) => (
56
+ <div onClick={onSelect}>{item.name}</div>
57
+ ),
58
+ noResultsComponent: () => <span>No results</span>,
59
+ },
60
+ ]}
61
+ />
43
62
  </InstantSearch>
44
63
  )
45
64
  }
46
65
  ```
47
66
 
67
+ ### Navigation is `getURL`'s job, not the item component's
68
+
69
+ - `indices[].getURL` — `(item) => string`, a route that already exists; don't invent one.
70
+ It navigates on click **and** on Enter over a highlighted item, and records the click
71
+ event. Without it, Enter on a highlight does nothing.
72
+ - `itemComponent` renders content only: **no `<a>`, no `href`, no `onClick` beyond the
73
+ `onSelect` it is handed, never `stopPropagation`.** The widget's `<li>` owns the click —
74
+ it sends the Insights `click`, closes the panel, clears the active option. Wrapping the
75
+ item in a link or a router `<Link>` swallows all three while still navigating: looks
76
+ correct, click analytics never fire, panel stays open behind the new page.
77
+ - `noResultsComponent` on every index, or a query matching nothing renders an empty box.
78
+ - `getSearchPageURL` sends Enter with nothing highlighted to a full results page; pass it
79
+ only if the app has one. `.getQuery` is optional. Leave `showQuerySuggestions`,
80
+ `showPromptSuggestions`, `showRecent`, and `aiMode` unset — each needs an index or
81
+ feature (query suggestions, recent-search storage, AI chat) this project lacks.
82
+ - TypeScript: type records through the generic, not casts inside `itemComponent` —
83
+ `type ProductHit = Hit<{ title: string; handle: string }>` (`Hit` from
84
+ `instantsearch.js`), then `<Autocomplete<ProductHit> ... />` types `item`, `getURL`, and
85
+ `itemComponent`.
86
+
87
+ ### Styling the panel
88
+
89
+ The widget ships no CSS. Install `instantsearch.css` and import a theme once, from the
90
+ component module or the app's global stylesheet:
91
+
92
+ ```tsx
93
+ import 'instantsearch.css/themes/satellite.css'
94
+ ```
95
+
96
+ The theme and your `.ais-*` overrides must be **global**: never route either through a CSS
97
+ Module, a scoped style block (Astro `<style>`, Vue `<style scoped>`, Svelte `<style>`), or
98
+ any styled-components-style wrapper that rewrites selectors. Scoping renames every
99
+ selector and the widget builds its DOM at runtime, so nothing matches — unstyled UI, no
100
+ error, build passes. Keep them in a global stylesheet, or add your own class names via
101
+ `classNames` and style those.
102
+
103
+ The theme ships a dark palette behind `:root[data-theme=dark], .dark`: in a dark app set
104
+ `data-theme="dark"` on `<html>` or render under a `.dark` ancestor, then adjust — don't
105
+ redefine the light palette by hand.
106
+
107
+ Palette lives in `--ais-*` custom properties on `:root` as **RGB triplets consumed through
108
+ `rgba()`**, not CSS colors — override with triplets or every widget color breaks:
109
+
110
+ ```css
111
+ :root {
112
+ --ais-background-color-rgb: 24, 24, 27; /* not #18181b */
113
+ --ais-text-color-rgb: 244, 244, 245;
114
+ --ais-primary-color-rgb: 129, 140, 248;
115
+ }
116
+ ```
117
+
118
+ Everything else is plain CSS against the widget's classes, or the `classNames` /
119
+ `indices[].classNames` props to add your own names to the same elements. Who owns what:
120
+
121
+ - `.ais-Autocomplete` — root. Its **container** sets the widget's width; don't size the
122
+ widget from the input.
123
+ - `.ais-AutocompleteForm` — visible frame: background, border, radius, focus ring
124
+ (`:focus-within`). The input's pill goes here.
125
+ - `.ais-AutocompleteInput` — text only; theme sets `border: 0`, `background: none`,
126
+ `width: 100%`. A border or width here leaves the form's frame drawn around it and
127
+ changes no widths.
128
+ - `.ais-AutocompletePanel` (+ `--open`), `.ais-AutocompletePanelLayout` — dropdown surface
129
+ and its scroll container.
130
+ - `.ais-AutocompleteIndex`, `-IndexHeader`, `-IndexList`, `-IndexItem`, `-IndexNoResults`
131
+ — one index's results; the active item carries `aria-selected="true"`.
132
+ - `.ais-AutocompleteItemWrapper`, `-ItemContent`, `-ItemIcon` — inside one result.
133
+ - `.ais-AutocompleteSubmitButton`, `-ClearButton`, `-LoadingIndicator`, `-Label` — form
134
+ controls.
135
+ - `.ais-AutocompleteDetached*` — detached mode (below).
136
+
137
+ Style nothing outside that list: `.ais-SearchBox-*` belongs to `SearchBox` and does
138
+ nothing here. Verify names against the installed version:
139
+ `grep -o 'ais-Autocomplete[A-Za-z-]*' node_modules/instantsearch.css/themes/satellite.css | sort -u`.
140
+
141
+ The theme's panel is `position: absolute; width: 100%`, so it inherits the input
142
+ container's width — a small input yields an unreadably narrow panel. Give it a min-width
143
+ and anchor it to the edge it opens from, or it grows off-screen:
144
+
145
+ ```css
146
+ .ais-AutocompletePanel {
147
+ min-width: 320px;
148
+ left: 0; /* right: 0; left: auto for an input aligned to the right */
149
+ }
150
+ ```
151
+
152
+ Below `(max-width: 680px)` the widget goes detached (fullscreen): the inline form becomes
153
+ a `.ais-AutocompleteDetachedSearchButton`, form and results move into an overlay
154
+ (`.ais-AutocompleteDetachedOverlay`, `.ais-AutocompleteDetachedContainer`,
155
+ `.ais-AutocompleteDetachedFormContainer`). So panel-width rules apply only above that
156
+ breakpoint, and styling the inline form leaves the mobile trigger button on the theme's
157
+ look unless you style it too. Change it with `detachedMediaQuery` or
158
+ `--ais-autocomplete-detached-media-query`; pass `''` to disable detached mode entirely.
159
+
48
160
  Do not inline `searchClient={algoliasearch(...)}` — keep the stable reference above.
49
161
 
50
162
  ## Insights (click analytics) — always enable it
@@ -53,17 +165,28 @@ The `insights` prop turns on the Insights middleware, which adds
53
165
  `clickAnalytics: true` to every query and keeps the `userToken` in sync between search
54
166
  and event calls. Never set `clickAnalytics` or `userToken` on `<Configure>` by hand.
55
167
 
56
- Use `insights={true}`. The middleware loads `search-insights` from the jsDelivr CDN
57
- itself; do not install the package or pass `insightsClient`.
168
+ Use `insights={true}` and nothing else. The middleware loads `search-insights` from the
169
+ jsDelivr CDN itself; do not install the package, pass `insightsClient`, or declare
170
+ `window.aa`.
58
171
 
59
- If a Content-Security-Policy blocks jsDelivr, the CDN script cannot load. In that
60
- case, install `search-insights` from npm and pass its default export as
172
+ Everything below is a **conditional fallback** — apply one only where its condition
173
+ actually holds here (a CSP you can point at, a user id the app already has). Otherwise
174
+ `insights={true}` is the whole implementation.
175
+
176
+ Only if the project has a Content-Security-Policy blocking jsDelivr (the CDN script then
177
+ cannot load): install `search-insights` from npm and pass its default export as
61
178
  `insightsClient`:
62
179
 
63
180
  ```tsx
64
181
  import aa from 'search-insights'
65
182
 
66
- <InstantSearch insights={{ insightsClient: aa }} />
183
+ export function Search() {
184
+ return (
185
+ <InstantSearch insights={{ insightsClient: aa }}>
186
+ {/* widgets */}
187
+ </InstantSearch>
188
+ )
189
+ }
67
190
  ```
68
191
 
69
192
  The middleware calls `aa('init', ...)` itself with the app ID and search key, so no
@@ -83,8 +206,13 @@ policy allows it:
83
206
 
84
207
  ### Click and conversion events
85
208
 
86
- `<Hits>` and `<InfiniteHits>` send `view` and `click` events on their own. Two cases
87
- need explicit wiring:
209
+ With `insights={true}`, `<Autocomplete>` sends `view` (on render) and `click` (from the
210
+ result element's own handler) itself. Its `itemComponent` receives
211
+ `{ item, onSelect, onApply }` and **no `sendEvent`** — nothing to wire. Call `onSelect`
212
+ for the widget's selection behavior (close the panel, record a recent search); the click
213
+ event fires either way.
214
+
215
+ `<Hits>` and `<InfiniteHits>` behave the same way. Two cases there need explicit wiring:
88
216
 
89
217
  - A custom `hitComponent` whose click target is your own element — it receives a
90
218
  `sendEvent` prop.
@@ -20,19 +20,68 @@ const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
20
20
 
21
21
  ## Vue (`vue-instantsearch` v4)
22
22
 
23
+ Use `ais-autocomplete` for the query and results state. It is **headless**: its default
24
+ slot provides only `currentRefinement`, `indices`, and `refine` — it renders nothing and
25
+ handles no focus, blur, keyboard, or ARIA. Build all of it yourself: panel closed until
26
+ the input is focused and has a query; close on Escape, blur, or outside click; active-option
27
+ highlight on Up/Down with Enter to select; combobox ARIA on the input; click events via
28
+ each index's `sendEvent`.
29
+
23
30
  ```vue
24
31
  <template>
25
- <ais-instant-search
26
- :search-client="searchClient"
27
- :index-name="ALGOLIA_INDEX_NAME"
28
- :insights="true"
29
- >
30
- <ais-search-box />
31
- <ais-hits />
32
- </ais-instant-search>
32
+ <div ref="containerRef" class="search" @focusout="onFocusOut">
33
+ <ais-instant-search
34
+ :search-client="searchClient"
35
+ :index-name="ALGOLIA_INDEX_NAME"
36
+ :insights="true"
37
+ >
38
+ <ais-autocomplete>
39
+ <template v-slot="{ currentRefinement, indices, refine }">
40
+ <input
41
+ type="search"
42
+ role="combobox"
43
+ aria-autocomplete="list"
44
+ aria-controls="search-panel"
45
+ :aria-expanded="isOpen(currentRefinement)"
46
+ :aria-activedescendant="
47
+ active >= 0 ? `search-option-${active}` : undefined
48
+ "
49
+ :value="currentRefinement"
50
+ @focus="open = true"
51
+ @input="onInput(refine, $event)"
52
+ @keydown.down.prevent="move(1, indices)"
53
+ @keydown.up.prevent="move(-1, indices)"
54
+ @keydown.enter.prevent="select(indices, active)"
55
+ @keydown.esc="close"
56
+ />
57
+ <ul
58
+ v-show="isOpen(currentRefinement)"
59
+ id="search-panel"
60
+ class="search-panel"
61
+ role="listbox"
62
+ aria-label="Search results"
63
+ >
64
+ <li
65
+ v-for="(hit, i) in indices[0]?.hits ?? []"
66
+ :id="`search-option-${i}`"
67
+ :key="hit.objectID"
68
+ role="option"
69
+ :aria-selected="i === active"
70
+ :class="{ 'is-active': i === active }"
71
+ @mousedown.prevent="select(indices, i)"
72
+ @mousemove="active = i"
73
+ >
74
+ <ais-highlight attribute="name" :hit="hit" />
75
+ </li>
76
+ </ul>
77
+ </template>
78
+ </ais-autocomplete>
79
+ </ais-instant-search>
80
+ </div>
33
81
  </template>
34
82
 
35
83
  <script setup>
84
+ import { ref } from 'vue'
36
85
  import { liteClient as algoliasearch } from 'algoliasearch/lite'
37
86
  import {
38
87
  ALGOLIA_APP_ID,
@@ -41,9 +90,71 @@ import {
41
90
  } from '<the project's shared config module>'
42
91
 
43
92
  const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
93
+ const open = ref(false)
94
+ const active = ref(-1)
95
+ const containerRef = ref(null)
96
+
97
+ function isOpen(currentRefinement) {
98
+ return open.value && Boolean(currentRefinement)
99
+ }
100
+
101
+ function onInput(refine, event) {
102
+ open.value = true
103
+ active.value = -1
104
+ refine(event.currentTarget.value)
105
+ }
106
+
107
+ function move(delta, indices) {
108
+ const hits = indices[0]?.hits ?? []
109
+ if (!hits.length) return
110
+ open.value = true
111
+ const next = active.value + delta
112
+ active.value = next < 0 ? hits.length - 1 : next % hits.length
113
+ }
114
+
115
+ function select(indices, index) {
116
+ const hit = indices[0]?.hits?.[index]
117
+ if (!hit) return
118
+ indices[0].sendEvent('click', hit, 'Hit Clicked')
119
+ close()
120
+ // TODO: navigate to this hit's page, where the app has one
121
+ }
122
+
123
+ function close() {
124
+ open.value = false
125
+ active.value = -1
126
+ }
127
+
128
+ function onFocusOut(event) {
129
+ if (!containerRef.value?.contains(event.relatedTarget)) close()
130
+ }
44
131
  </script>
45
132
  ```
46
133
 
134
+ Details that break accessibility if changed:
135
+
136
+ - `role="combobox"` on the `input`, not a wrapper — with `aria-controls` at the popup's
137
+ `id` and `aria-activedescendant` at the active option's `id`. Focus stays on the input
138
+ while the arrows move the highlight.
139
+ - The `role="listbox"` element must be the direct parent of the `role="option"` elements;
140
+ a `ul` inside a `listbox` div, or options one level deeper, breaks the a11y tree.
141
+ - `v-show`, not `v-if`, so the `aria-activedescendant` target stays in the DOM.
142
+ - `@mousedown.prevent` on an option, not `@click` — selection must land before `focusout`
143
+ closes the panel.
144
+ - `focusout` on the wrapper is the outside-click dismissal (`event.relatedTarget` is
145
+ `null` for a click on non-focusable chrome). Don't add a `document` click listener on
146
+ top — it fights `@mousedown.prevent`.
147
+ - `.is-active` needs a visible style, not just `aria-selected` — it is a keyboard user's
148
+ only focus feedback, since focus never leaves the input.
149
+
150
+ The panel is your own markup with no widget theme to inherit: give `.search-panel` a
151
+ background from the app's theme tokens (never transparent over page content), text
152
+ clearing 4.5:1 against it, `position: absolute` with `min-width: 320px` anchored to the
153
+ input edge it opens from, and a `max-height` with `overflow-y: auto`.
154
+
155
+ `indices` supports federated search across indices; this project has one, so `indices[0]`
156
+ is always the one to read.
157
+
47
158
  Register the widgets via the InstantSearch Vue plugin in your app entry:
48
159
 
49
160
  ```js
@@ -57,12 +168,15 @@ The `insights` prop turns on the Insights middleware, which adds
57
168
  `clickAnalytics: true` to every query and keeps the `userToken` in sync between search
58
169
  and event calls. Never set `clickAnalytics` or `userToken` on `ais-configure` by hand.
59
170
 
60
- Use `:insights="true"`. The middleware loads `search-insights` from the jsDelivr CDN itself; do not install
61
- the package or pass `insightsClient`.
171
+ Use `:insights="true"` and nothing else. The middleware loads `search-insights` from the
172
+ jsDelivr CDN itself; do not install the package, pass `insightsClient`, or declare `window.aa`.
62
173
 
63
- If a Content-Security-Policy blocks jsDelivr, the CDN script cannot load. In that
64
- case, install `search-insights` from npm and pass its default export as
65
- `insightsClient`:
174
+ Everything below is a **conditional fallback** — apply one only where its condition actually holds
175
+ here (a CSP you can point at, a user id the app already has). Otherwise `:insights="true"` is the
176
+ whole implementation.
177
+
178
+ Only if the project has a Content-Security-Policy blocking jsDelivr (the CDN script then cannot
179
+ load): install `search-insights` from npm and pass its default export as `insightsClient`:
66
180
 
67
181
  ```js
68
182
  import aa from 'search-insights'
@@ -85,19 +199,18 @@ const insights = { insightsInitParams: { useCookie: true } } // then :insights="
85
199
 
86
200
  ### Click and conversion events
87
201
 
88
- `ais-hits` sends `view` and `click` events on its own. A custom `item` slot whose
89
- click target is your own element needs the slot's `sendEvent`:
202
+ `ais-autocomplete` wraps the same connector as the other flavors, so `view` fires on its
203
+ own when results render. `click` does not — the option markup is yours: call the index's
204
+ `sendEvent` from your own option handler, as in the setup example above.
90
205
 
91
206
  ```vue
92
- <ais-hits>
93
- <template v-slot:item="{ item, sendEvent }">
94
- <a :href="item.url" @click="sendEvent('click', item, 'Hit Clicked')">
95
- {{ item.name }}
96
- </a>
97
- </template>
98
- </ais-hits>
207
+ <li @mousedown.prevent="indices[0].sendEvent('click', hit, 'Hit Clicked')">
99
208
  ```
100
209
 
210
+ `ais-hits` (if the app also has a full results page) sends both itself; only a custom
211
+ `item` slot whose click target is your own element needs wiring — that slot provides
212
+ `sendEvent`.
213
+
101
214
  Conversions are business actions (add to cart, signup, booking), so wire them at the
102
215
  real action, not at render:
103
216
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.44.0",
3
+ "version": "0.45.0-rc.141.320",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {