@algolia/wizard 0.69.0 → 0.71.0
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
|
@@ -1969,7 +1969,7 @@ function identify(traits) {
|
|
|
1969
1969
|
// package.json
|
|
1970
1970
|
var package_default = {
|
|
1971
1971
|
name: "@algolia/wizard",
|
|
1972
|
-
version: "0.
|
|
1972
|
+
version: "0.71.0",
|
|
1973
1973
|
description: "Magically implement Algolia functionality in your codebase",
|
|
1974
1974
|
type: "module",
|
|
1975
1975
|
engines: {
|
|
@@ -3440,6 +3440,12 @@ var DEFAULT_TOOL_LIMITS = {
|
|
|
3440
3440
|
match: 100,
|
|
3441
3441
|
shell: 50
|
|
3442
3442
|
};
|
|
3443
|
+
var EMPTY_TOOL_COUNTS = {
|
|
3444
|
+
list: 0,
|
|
3445
|
+
search: 0,
|
|
3446
|
+
read: 0,
|
|
3447
|
+
shell: 0
|
|
3448
|
+
};
|
|
3443
3449
|
var DEFAULT_SHELL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
3444
3450
|
async function refuseByDefault() {
|
|
3445
3451
|
return "reject";
|
|
@@ -3458,11 +3464,14 @@ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), sh
|
|
|
3458
3464
|
root: cwd,
|
|
3459
3465
|
cwd,
|
|
3460
3466
|
limits: { ...limits },
|
|
3461
|
-
counts: {
|
|
3467
|
+
counts: { ...EMPTY_TOOL_COUNTS },
|
|
3462
3468
|
shell: shell2,
|
|
3463
3469
|
reviewed: []
|
|
3464
3470
|
};
|
|
3465
3471
|
}
|
|
3472
|
+
function resetToolCounts(ctx) {
|
|
3473
|
+
Object.assign(ctx.counts, EMPTY_TOOL_COUNTS);
|
|
3474
|
+
}
|
|
3466
3475
|
|
|
3467
3476
|
// src/lib/tools/utils/runShell.ts
|
|
3468
3477
|
var SIGKILL_DELAY_MS = 5e3;
|
|
@@ -4128,10 +4137,21 @@ var MISSING_REPORT_STATUS_ERROR_MESSAGE = "Agent finished without calling report
|
|
|
4128
4137
|
var MISSING_REPORT_USER_MESSAGE = "This step ran into a problem finishing. Run the wizard again to retry it.";
|
|
4129
4138
|
var REPORT_STATUS_RETRIES = 2;
|
|
4130
4139
|
var ATTEMPT_NUMBER_OFFSET = 1;
|
|
4140
|
+
var INITIAL_ATTEMPT = 0;
|
|
4131
4141
|
var PROVIDER_ERROR_USER_MESSAGE = "The AI service had trouble responding. Run the wizard again to retry this step.";
|
|
4142
|
+
var MissingReportError = class extends Error {
|
|
4143
|
+
atStepLimit;
|
|
4144
|
+
constructor(atStepLimit) {
|
|
4145
|
+
super(MISSING_REPORT_STATUS_ERROR_MESSAGE);
|
|
4146
|
+
this.atStepLimit = atStepLimit;
|
|
4147
|
+
}
|
|
4148
|
+
};
|
|
4149
|
+
function isMissingReportRetry(kind) {
|
|
4150
|
+
return kind === "missingReport" || kind === "missingReportAtStepLimit";
|
|
4151
|
+
}
|
|
4132
4152
|
function retryKind(err) {
|
|
4133
|
-
if (err instanceof
|
|
4134
|
-
return "missingReport";
|
|
4153
|
+
if (err instanceof MissingReportError) {
|
|
4154
|
+
return err.atStepLimit ? "missingReportAtStepLimit" : "missingReport";
|
|
4135
4155
|
}
|
|
4136
4156
|
if (NoOutputGeneratedError.isInstance(err)) return "transientProvider";
|
|
4137
4157
|
if (APICallError.isInstance(err) && err.isRetryable) {
|
|
@@ -4140,37 +4160,42 @@ function retryKind(err) {
|
|
|
4140
4160
|
return null;
|
|
4141
4161
|
}
|
|
4142
4162
|
function exhaustedError(kind, err) {
|
|
4143
|
-
return kind
|
|
4163
|
+
return isMissingReportRetry(kind) ? new Error(MISSING_REPORT_USER_MESSAGE) : new Error(PROVIDER_ERROR_USER_MESSAGE, { cause: err });
|
|
4144
4164
|
}
|
|
4145
4165
|
async function runAgent(req) {
|
|
4146
|
-
|
|
4166
|
+
let retryReason = null;
|
|
4167
|
+
for (let attempt = INITIAL_ATTEMPT; attempt <= REPORT_STATUS_RETRIES; attempt++) {
|
|
4147
4168
|
try {
|
|
4148
|
-
return await runAgentAttempt(req, attempt);
|
|
4169
|
+
return await runAgentAttempt(req, attempt, retryReason);
|
|
4149
4170
|
} catch (err) {
|
|
4150
4171
|
const kind = retryKind(err);
|
|
4151
4172
|
if (!kind) throw err;
|
|
4152
4173
|
if (attempt === REPORT_STATUS_RETRIES) throw exhaustedError(kind, err);
|
|
4174
|
+
retryReason = kind === "transientProvider" && isMissingReportRetry(retryReason) ? retryReason : kind;
|
|
4153
4175
|
logger.warn(
|
|
4154
4176
|
{ attempt: attempt + 1, err },
|
|
4155
|
-
kind
|
|
4177
|
+
isMissingReportRetry(kind) ? "retrying runAgent after missing reportStatus" : "retrying runAgent after a transient provider error"
|
|
4156
4178
|
);
|
|
4157
4179
|
}
|
|
4158
4180
|
}
|
|
4159
4181
|
throw new Error("unreachable");
|
|
4160
4182
|
}
|
|
4161
|
-
async function runAgentAttempt(req, attempt) {
|
|
4183
|
+
async function runAgentAttempt(req, attempt, retryReason) {
|
|
4162
4184
|
const start = Date.now();
|
|
4163
4185
|
const profileName = req.modelProfile ?? "implementation" /* implementation */;
|
|
4164
4186
|
const profile = getModelProfile(profileName);
|
|
4165
|
-
const
|
|
4187
|
+
const profileOptions = providerOptionsForProfile(profileName);
|
|
4188
|
+
const forceReport = retryReason === "missingReport";
|
|
4189
|
+
const modelOptions = forceReport ? { thinking: { type: "disabled" } } : profileOptions;
|
|
4166
4190
|
logger.info(
|
|
4167
4191
|
{
|
|
4168
4192
|
startedAt: new Date(start).toISOString(),
|
|
4169
4193
|
operation: req.operation,
|
|
4170
4194
|
profile: profileName,
|
|
4171
4195
|
model: profile.model,
|
|
4172
|
-
effort: profile.effort,
|
|
4173
|
-
thinking: profile.thinking
|
|
4196
|
+
effort: forceReport ? void 0 : profile.effort,
|
|
4197
|
+
thinking: forceReport ? "disabled" : profile.thinking,
|
|
4198
|
+
retryReason
|
|
4174
4199
|
},
|
|
4175
4200
|
"runAgent started"
|
|
4176
4201
|
);
|
|
@@ -4184,6 +4209,7 @@ async function runAgentAttempt(req, attempt) {
|
|
|
4184
4209
|
fetch: proxyFetch
|
|
4185
4210
|
});
|
|
4186
4211
|
const toolContext = req.toolContext ?? createToolContext();
|
|
4212
|
+
if (attempt > INITIAL_ATTEMPT) resetToolCounts(toolContext);
|
|
4187
4213
|
const readTools = ["readFile", "searchFiles", "listFiles"];
|
|
4188
4214
|
const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
|
|
4189
4215
|
const instructions = [
|
|
@@ -4195,8 +4221,14 @@ async function runAgentAttempt(req, attempt) {
|
|
|
4195
4221
|
// Last so a retry does not bust the cached tools+system prefix from
|
|
4196
4222
|
// the first attempt. Inspect-and-continue, not start over: implement
|
|
4197
4223
|
// shares the worktree and toolContext across attempts.
|
|
4198
|
-
...
|
|
4199
|
-
"
|
|
4224
|
+
...retryReason === "missingReport" ? hasReadTools ? [
|
|
4225
|
+
"The previous attempt ended without reportStatus. Do not repeat side effects. Use the available read tools to inspect existing work if needed, then call reportStatus."
|
|
4226
|
+
] : [
|
|
4227
|
+
"The previous attempt ended without reportStatus. Do not repeat side effects. Call reportStatus now."
|
|
4228
|
+
] : retryReason === "missingReportAtStepLimit" ? [
|
|
4229
|
+
"The previous attempt reached its step limit without reportStatus and may be incomplete. Inspect existing work, finish only the remaining work, avoid repeated side effects, and call reportStatus."
|
|
4230
|
+
] : attempt > 0 ? [
|
|
4231
|
+
"This run is a retry after a transient AI service error. Files, shell commands, or ingestion may already have been applied in this workspace \u2014 inspect what is already there and continue from it rather than repeating that work."
|
|
4200
4232
|
] : []
|
|
4201
4233
|
];
|
|
4202
4234
|
const agent = new ToolLoopAgent({
|
|
@@ -4304,7 +4336,8 @@ async function runAgentAttempt(req, attempt) {
|
|
|
4304
4336
|
},
|
|
4305
4337
|
"Agent finished without calling reportStatus"
|
|
4306
4338
|
);
|
|
4307
|
-
|
|
4339
|
+
const incomplete = steps.length >= profile.maxSteps || lastStep?.finishReason !== "stop";
|
|
4340
|
+
throw new MissingReportError(incomplete);
|
|
4308
4341
|
}
|
|
4309
4342
|
const result = report.output;
|
|
4310
4343
|
if (result.status !== "success") {
|
|
@@ -4426,10 +4459,14 @@ var MODE_CONFIG = {
|
|
|
4426
4459
|
},
|
|
4427
4460
|
searchImplementation: {
|
|
4428
4461
|
instructions: [
|
|
4429
|
-
"Analyze the codebase to determine the single best
|
|
4430
|
-
"
|
|
4462
|
+
"Analyze the codebase to determine the single best existing file in which to mount search UI.",
|
|
4463
|
+
"Inspect the layout hierarchy, routes, and the candidate file together with its parent and sibling components; do not choose a location from its filename alone.",
|
|
4464
|
+
"Identify the pages and components that display or browse the indexed domain content, plus any existing search, filter, or navigation controls. Details of the confirmed entity are appended below when the wizard already knows them; otherwise infer the likely indexed domain content from the codebase.",
|
|
4465
|
+
"Match placement to search scope: put content-specific search near the corresponding browse/list controls; use a shared header or navigation component only when search is genuinely app-wide and fits its visual hierarchy.",
|
|
4466
|
+
"Account for available space, alignment, and behavior at the app's existing responsive breakpoints so the control will not crowd or displace primary navigation.",
|
|
4467
|
+
"Prefer an existing search control with the same content scope when one can be replaced without removing unrelated filters or navigation.",
|
|
4431
4468
|
"Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
|
|
4432
|
-
'
|
|
4469
|
+
'Spend your reads on the layout entry point, the candidate file with its parent and siblings, and the UI that lists the indexed content \u2014 around a dozen targeted reads or searches, not a repo-wide crawl. Stop once one location is clearly best, and do not guess: if none is, say "unknown".',
|
|
4433
4470
|
"Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
|
|
4434
4471
|
"When done, call reportStatus"
|
|
4435
4472
|
],
|
|
@@ -4684,7 +4721,8 @@ import z26 from "zod";
|
|
|
4684
4721
|
var confirmEntitiesSchema = z26.object({
|
|
4685
4722
|
// Final detection — the focused re-run may supersede project-scan's.
|
|
4686
4723
|
ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
|
|
4687
|
-
confirmedEntities: confirmedEntitiesFieldSchema
|
|
4724
|
+
confirmedEntities: confirmedEntitiesFieldSchema,
|
|
4725
|
+
searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional()
|
|
4688
4726
|
});
|
|
4689
4727
|
var SUGGEST_ENTITY = "Suggest a different entity\u2026";
|
|
4690
4728
|
function usedDetectedBackend(detected, confirmed2) {
|
|
@@ -4807,7 +4845,26 @@ async function confirmEntities(ctx) {
|
|
|
4807
4845
|
attribute_count: summary[0]?.attributes.length ?? 0,
|
|
4808
4846
|
used_detected_back_end: usedDetectedBackend(scan.languages, languages)
|
|
4809
4847
|
});
|
|
4810
|
-
|
|
4848
|
+
let searchImplementationAnalysis = scan.searchImplementationAnalysis;
|
|
4849
|
+
try {
|
|
4850
|
+
const placement = await runAnalysis("searchImplementation", [
|
|
4851
|
+
`The user confirmed this indexed entity: ${JSON.stringify(confirmed2[0])}. Determine placement specifically for this content and its existing user task; do not substitute another domain found in the repository.`
|
|
4852
|
+
]);
|
|
4853
|
+
searchImplementationAnalysis = placement.searchImplementationAnalysis;
|
|
4854
|
+
} catch (err) {
|
|
4855
|
+
logger.warn({ err }, "confirmEntities: placement refresh failed");
|
|
4856
|
+
}
|
|
4857
|
+
if (searchImplementationAnalysis) {
|
|
4858
|
+
ctx.setUserInput(
|
|
4859
|
+
"searchImplementationAnalysis",
|
|
4860
|
+
searchImplementationAnalysis
|
|
4861
|
+
);
|
|
4862
|
+
}
|
|
4863
|
+
return {
|
|
4864
|
+
ingestionAnalysis: entities,
|
|
4865
|
+
confirmedEntities: confirmed2,
|
|
4866
|
+
searchImplementationAnalysis
|
|
4867
|
+
};
|
|
4811
4868
|
}
|
|
4812
4869
|
}
|
|
4813
4870
|
|
|
@@ -5133,19 +5190,37 @@ function ingestionInstructions(input) {
|
|
|
5133
5190
|
];
|
|
5134
5191
|
}
|
|
5135
5192
|
function searchInstructions(input) {
|
|
5136
|
-
const
|
|
5137
|
-
const
|
|
5138
|
-
|
|
5193
|
+
const indexedEntity = input.findings.confirmedEntities?.[0];
|
|
5194
|
+
const indexedContentMessage = indexedEntity ? `Indexed content context: ${JSON.stringify({
|
|
5195
|
+
entity: indexedEntity.name,
|
|
5196
|
+
attributes: indexedEntity.attributes,
|
|
5197
|
+
sourcePaths: indexedEntity.paths
|
|
5198
|
+
})}. Use the source and the existing UI that displays this entity to understand the search task, result content, and appropriate scope.` : null;
|
|
5199
|
+
const mountCandidateMessage = input.searchLocation ? `The confirmed-content placement analysis suggested "${input.searchLocation}" as a mount candidate. Before editing, read that file and inspect its parent layout, sibling controls, and the relevant content UI. If the evidence confirms it, import and render the new search component there; otherwise mount it in the better existing file. In all cases, mount the component before finishing; do not leave an unrendered component.` : "The scan found no reliable mount candidate. Determine the best existing location from the layout hierarchy and the UI that displays the indexed content, then mount the new search component there; do not default to a header or navigation component or leave an unrendered component.";
|
|
5200
|
+
const placementMessage = "Place content-specific search beside the heading or controls for the corresponding browse/listing task. Use shared navigation only for genuinely app-wide search and only when it fits the available space and responsive layout without crowding primary actions.";
|
|
5201
|
+
const replacementMessage = "If a search control with the same content scope and purpose already exists, replace its usage with the new component while preserving its intentional placement, and delete the implementation it supersedes. Do not remove or replace unrelated filters, navigation, or differently scoped search.";
|
|
5202
|
+
const accessibilityInstructions = [
|
|
5203
|
+
"Target WCAG 2.2 AA. Use semantic search structure and give the input a persistent accessible name: prefer a visible associated label and use the project's visually-hidden utility where the layout has no room for one. A placeholder or a labeled search landmark does not name the input. When the input uses aria-labelledby, every referenced element must exist and contain meaningful, content-specific text. Preserve a widget's generated label when it provides one and make it content-specific through supported options; if the installed widget exposes no such option, use its documented connector or headless API to render a labeled input instead of patching generated DOM.",
|
|
5204
|
+
"Preserve widget-provided combobox, popup, and item roles, relationships, and keyboard behavior; for a headless widget, implement the documented semantics instead of inventing different ARIA. Tab must reach the input and controls, Arrow keys move through results, Enter selects, and Escape dismisses without trapping or losing focus.",
|
|
5205
|
+
"Meet 4.5:1 contrast for normal text, including input values, placeholders, and result text, and 3:1 for large text, meaningful icons, control boundaries, and focus indicators against adjacent colors. Keep focus visible and unobscured, and never communicate active, loading, empty, or error states by color alone.",
|
|
5206
|
+
"Keep loading, empty, and error status text perceivable to assistive technology. Do not assume a widget announces these states: use documented announcements when present, otherwise add a persistent polite live region driven by the current search status, results, and errors.",
|
|
5207
|
+
"Reflow at a 320 CSS-pixel viewport without horizontal page scrolling, keep the results panel readable and on-screen whatever the width of the input it hangs off, keep interactive targets at least 24 by 24 CSS pixels or sufficiently spaced, and respect prefers-reduced-motion for any animation you add."
|
|
5208
|
+
];
|
|
5139
5209
|
const packageManagedInstructions = input.frontendHasPackageJson ? packageSearchInstructions(input) : cdnSearchInstructions(input);
|
|
5140
5210
|
return [
|
|
5141
5211
|
"Implement an in-app Algolia search experience.",
|
|
5142
|
-
|
|
5212
|
+
indexedContentMessage ?? "",
|
|
5143
5213
|
`Build the search UI for ${input.searchUiTarget}.`,
|
|
5144
5214
|
"Create search UI only. Do not create or modify ingestion scripts, rake/manage/CLI tasks, migrations, seeders, or other data-loading code, even if the data looks incomplete.",
|
|
5215
|
+
mountCandidateMessage,
|
|
5216
|
+
placementMessage,
|
|
5217
|
+
replacementMessage,
|
|
5218
|
+
"Inspect the chosen mount area at the app's existing breakpoints before writing CSS. Preserve its visual hierarchy, alignment, spacing, and primary actions rather than forcing the search control into an arbitrary gap.",
|
|
5145
5219
|
...packageManagedInstructions,
|
|
5146
|
-
|
|
5147
|
-
"
|
|
5148
|
-
"
|
|
5220
|
+
...accessibilityInstructions,
|
|
5221
|
+
"Style the search surfaces through the widget's class and CSS-variable overrides (or, when hand-built, the app's existing theme tokens): give the results panel and the element that actually draws the input's border and background \u2014 for a widget, the form or wrapper it renders around the bare input \u2014 non-transparent surfaces, keep that frame distinguishable from the background behind it in every supported theme, and never assume a light surface or reuse the surrounding page's colors unchanged inside the panel.",
|
|
5222
|
+
"The panel takes its width from the input by default, so a small input makes it unreadably narrow: give it `min-width: min(320px, calc(100vw - 2rem))` independent of the input, cap its maximum width to the viewport, and anchor it to the input edge it opens from so widening does not push it off-screen.",
|
|
5223
|
+
"Match the application styles closely, including light, dark, and high-contrast themes the project already supports.",
|
|
5149
5224
|
"The summary should be extremely concise; do not mention manual testing steps."
|
|
5150
5225
|
];
|
|
5151
5226
|
}
|
|
@@ -5159,8 +5234,6 @@ function packageSearchInstructions(input) {
|
|
|
5159
5234
|
"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."
|
|
5160
5235
|
],
|
|
5161
5236
|
`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.`,
|
|
5162
|
-
`Import and render the new component in ${input.searchLocation ? `"${input.searchLocation}"` : "the best, 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.`,
|
|
5163
|
-
"If a search box already exists, replace its usage with an import and render of your new component; remove the old implementation.",
|
|
5164
5237
|
"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.",
|
|
5165
5238
|
"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.",
|
|
5166
5239
|
`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).`,
|
|
@@ -5171,21 +5244,29 @@ function packageSearchInstructions(input) {
|
|
|
5171
5244
|
];
|
|
5172
5245
|
}
|
|
5173
5246
|
function cdnSearchInstructions(input) {
|
|
5247
|
+
const entityName = input.findings.confirmedEntities?.[0]?.name;
|
|
5248
|
+
const placeholder = entityName ? `Search ${entityName} records...` : "Search indexed content...";
|
|
5174
5249
|
return [
|
|
5175
5250
|
`The target frontend has no package.json. Add the search container to its existing HTML or template, create separate classic config and search scripts, define window.${SEARCH_CONFIG_APP_ID}, window.${SEARCH_CONFIG_SEARCH_KEY}, and window.${SEARCH_CONFIG_INDEX_NAME} in the config script, and report that script as "searchConfigFile". Never create package.json, create a component, use imports or exports, or run npm, pnpm, yarn, or bun.`,
|
|
5176
5251
|
`Set window.${SEARCH_CONFIG_APP_ID} to "${input.appId}" and window.${SEARCH_CONFIG_INDEX_NAME} to "${input.targetIndex}".`,
|
|
5177
5252
|
input.searchKey ? `Set window.${SEARCH_CONFIG_SEARCH_KEY} to "${input.searchKey}".` : `A real search-only key could not be provisioned${input.searchKeyError ? ` (${input.searchKeyError})` : ""} \u2014 set window.${SEARCH_CONFIG_SEARCH_KEY} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a real one.`,
|
|
5178
5253
|
'Load these exact pinned tags in order before the config and search scripts: <script src="https://cdn.jsdelivr.net/npm/algoliasearch@5.59.0/dist/lite/builds/browser.umd.js" integrity="sha256-pduQHl1jn0IaN/BIpSotQm7Y5THdpKcX3Bw6xdRteRw=" crossorigin="anonymous"></script> then <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.117.0/dist/instantsearch.production.min.js" integrity="sha256-5zhKxAGeH7ThWfnmdD4pS1gsmafE2b8aWRsq8set/dc=" crossorigin="anonymous"></script>.',
|
|
5179
5254
|
'When using the InstantSearch theme, load this exact tag before the application styles: <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@8.22.1/themes/satellite-min.css" integrity="sha256-5/eHPZl63VYJSDVOKrOgGe/5+owUkX3uPAW6+XYeLNc=" crossorigin="anonymous">. Never use an unversioned or differently versioned theme URL, and never omit its integrity attribute.',
|
|
5180
|
-
`In that classic search script, use window['algoliasearch/lite'].liteClient and window.instantsearch. Add window.instantsearch.widgets.autocomplete as the only search widget. Configure it with container '#autocomplete', placeholder
|
|
5255
|
+
`In that classic search script, use window['algoliasearch/lite'].liteClient and window.instantsearch. Add window.instantsearch.widgets.autocomplete as the only search widget. Configure it with container '#autocomplete', the content-specific placeholder ${JSON.stringify(placeholder)}, and an indices array containing one object whose indexName is window.${SEARCH_CONFIG_INDEX_NAME}; put item and noResults functions in its templates option, implement getURL only for an existing route, and match the item template to the entity schema. Keep insights: true and call search.start(). Do not substitute searchBox or hits, omit indices, use imports or exports, or use type="module".`
|
|
5181
5256
|
];
|
|
5182
5257
|
}
|
|
5183
5258
|
function validationInstructions(input) {
|
|
5184
5259
|
return [
|
|
5185
5260
|
"Validate the Algolia search implementation by inspecting the project source.",
|
|
5261
|
+
`Indexed content context: ${JSON.stringify(input.findings.confirmedEntities?.[0] ?? null)}.`,
|
|
5262
|
+
`The confirmed placement candidate was "${input.searchLocation ?? "unknown"}", but verify the actual mount rather than assuming the candidate was used.`,
|
|
5263
|
+
"Inspect the generated search component, its actual mount file, that file's parent layout and sibling controls, and the relevant content UI. Do not rely only on lint, type checks, or tests.",
|
|
5186
5264
|
"Confirm that the application renders exactly one search bar. Check for an old search bar and duplicate mounts.",
|
|
5187
|
-
|
|
5188
|
-
"Confirm
|
|
5265
|
+
"Confirm content-specific search is placed with the corresponding content or task controls. Shared navigation is valid only for genuinely app-wide search that fits the visual hierarchy and responsive layout without crowding primary actions.",
|
|
5266
|
+
"Confirm the input has a persistent accessible name; widget-provided combobox, popup, and item relationships and keyboard behavior are preserved; and loading, empty, and error status text is perceivable to assistive technology.",
|
|
5267
|
+
"Inspect the CSS that actually applies to the generated DOM. Confirm the results panel and the element drawing the input frame have non-transparent surfaces, the input frame is distinguishable from its surrounding background in every supported theme, text meets 4.5:1 contrast, meaningful boundaries and focus indicators meet 3:1, and no state relies on color alone.",
|
|
5268
|
+
"Confirm the search experience reflows without horizontal page scrolling at 320 CSS pixels, keeps its panel on-screen, preserves visible focus, and respects reduced-motion preferences for any added animation.",
|
|
5269
|
+
"Confirm that the search component is reachable from and fits the corresponding content task at every supported breakpoint; require app-wide reachability only when the search scope is genuinely app-wide.",
|
|
5189
5270
|
"Do not run the test suite or any test command.",
|
|
5190
5271
|
"Do not modify files. Report each implementation issue as a concrete instruction for the next implementation pass.",
|
|
5191
5272
|
`Available type-check and lint tools: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
@@ -5193,8 +5274,10 @@ function validationInstructions(input) {
|
|
|
5193
5274
|
"The frontend has no package.json: never create one or run npm, pnpm, yarn, or bun. Validate the CDN implementation by inspecting its HTML or template and classic scripts for the search container, Algolia \u2192 InstantSearch \u2192 config \u2192 search load order, browser globals, autocomplete widget configuration, search.start(), and exact pinned CDN URLs and integrity attributes for every Algolia script or stylesheet. Source inspection is required even when the project has no automated checks. window.instantsearch.widgets.autocomplete is a real widget in the pinned InstantSearch build; do not report its use as an error, and do not fetch a remote bundle or URL to verify it."
|
|
5194
5275
|
] : [],
|
|
5195
5276
|
"Always call reportStatus with status=success after validation, even when sufficient=false.",
|
|
5196
|
-
"Set sufficient=
|
|
5197
|
-
"Set sufficient=
|
|
5277
|
+
"Set sufficient=false if the input's computed accessible name is empty or generic, including when aria-labelledby references a missing or empty element; a placeholder does not count as the label.",
|
|
5278
|
+
"Set sufficient=true when the search bar is unique and structurally complete and no placement or accessibility violation was found.",
|
|
5279
|
+
"Set sufficient=false when a required placement or accessibility property is observed to fail, the implementation is incomplete, or checks have implementation-caused failures; include concrete additionalInstructions for the next pass.",
|
|
5280
|
+
"If any required placement or accessibility property cannot be established with the available tools\u2014including contrast, keyboard behavior, 320-pixel reflow, or live-region status\u2014set sufficient=false and request concrete verification or a fix in additionalInstructions; do not let an unverified requirement pass as a manual follow-up.",
|
|
5198
5281
|
"If a check fails only in untouched code, set unrelatedFailure to a concise failure description."
|
|
5199
5282
|
];
|
|
5200
5283
|
}
|
|
@@ -5465,12 +5548,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5465
5548
|
const entities = ctx.getStepOutput(
|
|
5466
5549
|
"confirm-entities"
|
|
5467
5550
|
);
|
|
5468
|
-
const findings = {
|
|
5469
|
-
ingestionAnalysis: entities?.ingestionAnalysis ?? scan.ingestionAnalysis,
|
|
5470
|
-
searchImplementationAnalysis: scan.searchImplementationAnalysis,
|
|
5471
|
-
verification: scan.verification,
|
|
5472
|
-
confirmedEntities: entities?.confirmedEntities
|
|
5473
|
-
};
|
|
5474
5551
|
const language = {
|
|
5475
5552
|
languages: ctx.getStepOutput("confirm-language")?.languages ?? scan.languages,
|
|
5476
5553
|
frameworks: ctx.getStepOutput("confirm-framework")?.frameworks ?? scan.frameworks
|
|
@@ -5495,9 +5572,14 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5495
5572
|
if (useCases.includes("ingestion")) {
|
|
5496
5573
|
await mkdir5(join10(repoRoot, INGEST_DIR), { recursive: true });
|
|
5497
5574
|
}
|
|
5498
|
-
const
|
|
5499
|
-
|
|
5500
|
-
|
|
5575
|
+
const findings = normalizeFindingPaths({
|
|
5576
|
+
ingestionAnalysis: entities?.ingestionAnalysis ?? scan.ingestionAnalysis,
|
|
5577
|
+
searchImplementationAnalysis: entities?.searchImplementationAnalysis ?? scan.searchImplementationAnalysis,
|
|
5578
|
+
verification: scan.verification,
|
|
5579
|
+
confirmedEntities: entities?.confirmedEntities
|
|
5580
|
+
});
|
|
5581
|
+
const confirmed2 = findings.confirmedEntities;
|
|
5582
|
+
const searchLocation = findings.searchImplementationAnalysis;
|
|
5501
5583
|
let appId;
|
|
5502
5584
|
let ingestAppId;
|
|
5503
5585
|
if (useCases.includes("search")) {
|
|
@@ -5532,7 +5614,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5532
5614
|
}
|
|
5533
5615
|
}
|
|
5534
5616
|
const input = {
|
|
5535
|
-
findings
|
|
5617
|
+
findings,
|
|
5536
5618
|
confirmed: confirmed2,
|
|
5537
5619
|
searchLocation,
|
|
5538
5620
|
targetIndex,
|
|
@@ -5560,8 +5642,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5560
5642
|
[APP_ID_VAR]: ingestKeyAppId,
|
|
5561
5643
|
[API_KEY_VAR]: ingestWriteKey,
|
|
5562
5644
|
[INDEX_NAME_VAR]: targetIndex
|
|
5563
|
-
})) :
|
|
5564
|
-
const searchTools = makeToolContext(repoRoot);
|
|
5645
|
+
})) : makeToolContext(repoRoot);
|
|
5565
5646
|
async function runImplementationUseCase(currentUseCase, extraInstructions = [], isRetry = false) {
|
|
5566
5647
|
if (agentRuns > 0) ctx.recordStepExecution();
|
|
5567
5648
|
agentRuns += 1;
|
|
@@ -5575,23 +5656,29 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5575
5656
|
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
5576
5657
|
outputSchema: implementationOutputSchema,
|
|
5577
5658
|
modelProfile: isRetry ? "implementationRetry" /* implementationRetry */ : "implementation" /* implementation */,
|
|
5578
|
-
toolContext: currentUseCase === "ingestion" ? ingestionTools
|
|
5659
|
+
toolContext: currentUseCase === "ingestion" ? ingestionTools : makeToolContext(repoRoot)
|
|
5579
5660
|
});
|
|
5580
5661
|
}
|
|
5581
5662
|
async function runValidationUseCase(extraInstructions) {
|
|
5582
5663
|
if (agentRuns > 0) ctx.recordStepExecution();
|
|
5583
5664
|
agentRuns += 1;
|
|
5665
|
+
const writtenFiles = [...new Set(useWizard.getState().writtenFiles)].map(
|
|
5666
|
+
(file) => relative6(repoRoot, file)
|
|
5667
|
+
);
|
|
5584
5668
|
return runAgent({
|
|
5585
5669
|
operation: "search-validation",
|
|
5586
5670
|
instructions: buildAgentInstructions(
|
|
5587
5671
|
"validation",
|
|
5588
5672
|
input,
|
|
5589
|
-
|
|
5673
|
+
[
|
|
5674
|
+
writtenFiles.length ? `Files written during search implementation: ${JSON.stringify(writtenFiles)}. Inspect these first, then trace the actual import and render site.` : "The search agent reported no written files. Inspect the current diff and set sufficient=false if no complete mounted search implementation exists.",
|
|
5675
|
+
...extraInstructions
|
|
5676
|
+
]
|
|
5590
5677
|
),
|
|
5591
5678
|
tools: toolsForUseCase("validation"),
|
|
5592
5679
|
outputSchema: validationOutputSchema,
|
|
5593
5680
|
modelProfile: "validation" /* validation */,
|
|
5594
|
-
toolContext:
|
|
5681
|
+
toolContext: makeToolContext(repoRoot)
|
|
5595
5682
|
});
|
|
5596
5683
|
}
|
|
5597
5684
|
if (useCases.includes("ingestion")) {
|
|
@@ -5599,7 +5686,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5599
5686
|
const result = await runImplementationUseCase("ingestion");
|
|
5600
5687
|
summaries.push(formatSummary("ingestion", result.summary));
|
|
5601
5688
|
ingestCommand = result.ingestCommand;
|
|
5602
|
-
const ingestionContext = ingestionTools
|
|
5689
|
+
const ingestionContext = ingestionTools;
|
|
5603
5690
|
const executions = ingestionContext.shell.executions;
|
|
5604
5691
|
const {
|
|
5605
5692
|
run: ingestRun,
|
|
@@ -7502,7 +7589,7 @@ function delay(ms) {
|
|
|
7502
7589
|
// package.json with { type: 'json' }
|
|
7503
7590
|
var package_default2 = {
|
|
7504
7591
|
name: "@algolia/wizard",
|
|
7505
|
-
version: "0.
|
|
7592
|
+
version: "0.71.0",
|
|
7506
7593
|
description: "Magically implement Algolia functionality in your codebase",
|
|
7507
7594
|
type: "module",
|
|
7508
7595
|
engines: {
|
|
@@ -91,6 +91,40 @@ Ensure the attributes you map into the component match the entity. 'title' above
|
|
|
91
91
|
`instantsearch.js`), then `autocomplete<ProductHit>({ ... })` types `item`, `getURL`,
|
|
92
92
|
and the templates.
|
|
93
93
|
|
|
94
|
+
### Accessibility contract
|
|
95
|
+
|
|
96
|
+
The widget owns its combobox/grid/row relationships, active-option announcements, and
|
|
97
|
+
keyboard behavior. Keep its generated roles and accessible names intact; never patch its
|
|
98
|
+
DOM or add competing ARIA.
|
|
99
|
+
|
|
100
|
+
The widget renders the input and its label itself. Keep that association intact, and
|
|
101
|
+
verify that the generated label names the indexed content (`Search products`, not just
|
|
102
|
+
`Search`). A placeholder is only a hint, and the accessible name of a surrounding
|
|
103
|
+
`<search>` landmark does not name the input. If the installed widget exposes no supported
|
|
104
|
+
way to make its generated label content-specific, use its documented connector or
|
|
105
|
+
headless API to render an associated visible or visually-hidden label while preserving
|
|
106
|
+
the widget's generated input props and keyboard behavior; never patch its DOM after
|
|
107
|
+
rendering.
|
|
108
|
+
|
|
109
|
+
Use a content-specific placeholder, put the container in the app's existing named search
|
|
110
|
+
landmark or a `<search aria-label="Product search">`, and preserve any visible search
|
|
111
|
+
label already present in the layout. `translations` covers only the detached-mode
|
|
112
|
+
controls (`detachedCancelButtonText`, `detachedSearchButtonTitle`,
|
|
113
|
+
`detachedClearButtonTitle`) — set them in a non-English app.
|
|
114
|
+
|
|
115
|
+
The widget does not provide live announcements for loading, empty, or error states. Add
|
|
116
|
+
a persistent `aria-live="polite"` region driven by the current search status, results,
|
|
117
|
+
and errors, hidden with the app's visually-hidden utility if it should not show. Target
|
|
118
|
+
WCAG 2.2 AA and verify after styling that:
|
|
119
|
+
|
|
120
|
+
- input, placeholder, and result text clear 4.5:1 contrast;
|
|
121
|
+
- meaningful icons, control boundaries, and focus indicators clear 3:1 contrast;
|
|
122
|
+
- focus is visible and unobscured, and no state relies on color alone;
|
|
123
|
+
- all controls remain keyboard-operable and have 24 by 24 CSS pixel targets or enough
|
|
124
|
+
spacing around smaller targets;
|
|
125
|
+
- the experience reflows at a 320 CSS pixel viewport without horizontal page scrolling;
|
|
126
|
+
- any added animation respects `prefers-reduced-motion`.
|
|
127
|
+
|
|
94
128
|
### Styling the panel
|
|
95
129
|
|
|
96
130
|
The widget ships no CSS. Install `instantsearch.css` and import a theme once, from the
|
|
@@ -139,12 +173,15 @@ Everything else is plain CSS against the widget's classes, or `cssClasses` /
|
|
|
139
173
|
- `.ais-AutocompleteDetached*` — detached mode (below).
|
|
140
174
|
|
|
141
175
|
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
|
|
143
|
-
|
|
176
|
+
container's width — a small input yields an unreadably narrow panel. Give it a 320px
|
|
177
|
+
minimum when space permits, cap it to the viewport, and anchor it to the edge it opens
|
|
178
|
+
from. Give the form and panel non-transparent surfaces from the app's tokens, with the
|
|
179
|
+
form boundary distinguishable from the surrounding background in every supported theme:
|
|
144
180
|
|
|
145
181
|
```css
|
|
146
182
|
.ais-AutocompletePanel {
|
|
147
|
-
min-width: 320px;
|
|
183
|
+
min-width: min(320px, calc(100vw - 2rem));
|
|
184
|
+
max-width: calc(100vw - 2rem);
|
|
148
185
|
left: 0; /* right: 0; left: auto for an input aligned to the right */
|
|
149
186
|
}
|
|
150
187
|
```
|
|
@@ -83,6 +83,40 @@ Ensure the attributes you map into the component match the entity. 'title' above
|
|
|
83
83
|
`instantsearch.js`), then `<Autocomplete<ProductHit> ... />` types `item`, `getURL`, and
|
|
84
84
|
`itemComponent`.
|
|
85
85
|
|
|
86
|
+
### Accessibility contract
|
|
87
|
+
|
|
88
|
+
The widget owns its combobox/grid/row relationships, active-option announcements, and
|
|
89
|
+
keyboard behavior. Keep its generated roles and accessible names intact; never patch its
|
|
90
|
+
DOM or add competing ARIA.
|
|
91
|
+
|
|
92
|
+
The widget renders the input and its label itself. Keep that association intact, and
|
|
93
|
+
verify that the generated label names the indexed content (`Search products`, not just
|
|
94
|
+
`Search`). A placeholder is only a hint, and the accessible name of a surrounding
|
|
95
|
+
`<search>` landmark does not name the input. If the installed widget exposes no supported
|
|
96
|
+
way to make its generated label content-specific, use its documented connector or
|
|
97
|
+
headless API to render an associated visible or visually-hidden label while preserving
|
|
98
|
+
the widget's generated input props and keyboard behavior; never patch its DOM after
|
|
99
|
+
rendering.
|
|
100
|
+
|
|
101
|
+
Use a content-specific placeholder, put the widget in the app's existing named search
|
|
102
|
+
landmark or a `<search aria-label="Product search">`, and preserve any visible search
|
|
103
|
+
label already present in the layout. On `react-instantsearch` 7.24.0+, `translations`
|
|
104
|
+
covers the detached-mode controls (`detachedCancelButtonText`,
|
|
105
|
+
`detachedSearchButtonTitle`, `detachedClearButtonTitle`) — set them in a non-English app.
|
|
106
|
+
|
|
107
|
+
The widget does not provide live announcements for loading, empty, or error states. Add
|
|
108
|
+
a persistent `aria-live="polite"` region driven by the current search status, results,
|
|
109
|
+
and errors, hidden with the app's visually-hidden utility if it should not show. Target
|
|
110
|
+
WCAG 2.2 AA and verify after styling that:
|
|
111
|
+
|
|
112
|
+
- input, placeholder, and result text clear 4.5:1 contrast;
|
|
113
|
+
- meaningful icons, control boundaries, and focus indicators clear 3:1 contrast;
|
|
114
|
+
- focus is visible and unobscured, and no state relies on color alone;
|
|
115
|
+
- all controls remain keyboard-operable and have 24 by 24 CSS pixel targets or enough
|
|
116
|
+
spacing around smaller targets;
|
|
117
|
+
- the experience reflows at a 320 CSS pixel viewport without horizontal page scrolling;
|
|
118
|
+
- any added animation respects `prefers-reduced-motion`.
|
|
119
|
+
|
|
86
120
|
### Styling the panel
|
|
87
121
|
|
|
88
122
|
The widget ships no CSS. Install `instantsearch.css` and import a theme once, from the
|
|
@@ -131,12 +165,15 @@ Everything else is plain CSS against the widget's classes, or the `classNames` /
|
|
|
131
165
|
- `.ais-AutocompleteDetached*` — detached mode (below).
|
|
132
166
|
|
|
133
167
|
The theme's panel is `position: absolute; width: 100%`, so it inherits the input
|
|
134
|
-
container's width — a small input yields an unreadably narrow panel. Give it a
|
|
135
|
-
|
|
168
|
+
container's width — a small input yields an unreadably narrow panel. Give it a 320px
|
|
169
|
+
minimum when space permits, cap it to the viewport, and anchor it to the edge it opens
|
|
170
|
+
from. Give the form and panel non-transparent surfaces from the app's tokens, with the
|
|
171
|
+
form boundary distinguishable from the surrounding background in every supported theme:
|
|
136
172
|
|
|
137
173
|
```css
|
|
138
174
|
.ais-AutocompletePanel {
|
|
139
|
-
min-width: 320px;
|
|
175
|
+
min-width: min(320px, calc(100vw - 2rem));
|
|
176
|
+
max-width: calc(100vw - 2rem);
|
|
140
177
|
left: 0; /* right: 0; left: auto for an input aligned to the right */
|
|
141
178
|
}
|
|
142
179
|
```
|
|
@@ -37,14 +37,16 @@ each index's `sendEvent`.
|
|
|
37
37
|
>
|
|
38
38
|
<ais-autocomplete>
|
|
39
39
|
<template v-slot="{ currentRefinement, indices, refine }">
|
|
40
|
+
<label :for="inputId">Search products</label>
|
|
40
41
|
<input
|
|
42
|
+
:id="inputId"
|
|
41
43
|
type="search"
|
|
42
44
|
role="combobox"
|
|
43
45
|
aria-autocomplete="list"
|
|
44
|
-
aria-controls="
|
|
46
|
+
:aria-controls="panelId"
|
|
45
47
|
:aria-expanded="isOpen(currentRefinement)"
|
|
46
48
|
:aria-activedescendant="
|
|
47
|
-
active >= 0 ?
|
|
49
|
+
active >= 0 ? optionId(active) : undefined
|
|
48
50
|
"
|
|
49
51
|
:value="currentRefinement"
|
|
50
52
|
@focus="open = true"
|
|
@@ -54,16 +56,19 @@ each index's `sendEvent`.
|
|
|
54
56
|
@keydown.enter.prevent="select(indices, active)"
|
|
55
57
|
@keydown.esc="close"
|
|
56
58
|
/>
|
|
59
|
+
<p class="visually-hidden" aria-live="polite">
|
|
60
|
+
{{ statusMessage(currentRefinement, indices) }}
|
|
61
|
+
</p>
|
|
57
62
|
<ul
|
|
58
63
|
v-show="isOpen(currentRefinement)"
|
|
59
|
-
id="
|
|
64
|
+
:id="panelId"
|
|
60
65
|
class="search-panel"
|
|
61
66
|
role="listbox"
|
|
62
67
|
aria-label="Search results"
|
|
63
68
|
>
|
|
64
69
|
<li
|
|
65
70
|
v-for="(hit, i) in indices[0]?.hits ?? []"
|
|
66
|
-
:id="
|
|
71
|
+
:id="optionId(i)"
|
|
67
72
|
:key="hit.objectID"
|
|
68
73
|
role="option"
|
|
69
74
|
:aria-selected="i === active"
|
|
@@ -89,15 +94,28 @@ import {
|
|
|
89
94
|
ALGOLIA_INDEX_NAME,
|
|
90
95
|
} from '<the project's shared config module>'
|
|
91
96
|
|
|
97
|
+
const props = defineProps({
|
|
98
|
+
idPrefix: { type: String, required: true },
|
|
99
|
+
})
|
|
92
100
|
const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
93
101
|
const open = ref(false)
|
|
94
102
|
const active = ref(-1)
|
|
95
103
|
const containerRef = ref(null)
|
|
96
104
|
|
|
105
|
+
const inputId = `${props.idPrefix}-input`
|
|
106
|
+
const panelId = `${props.idPrefix}-panel`
|
|
107
|
+
const optionId = (index) => `${props.idPrefix}-option-${index}`
|
|
108
|
+
|
|
97
109
|
function isOpen(currentRefinement) {
|
|
98
110
|
return open.value && Boolean(currentRefinement)
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
function statusMessage(currentRefinement, indices) {
|
|
114
|
+
if (!isOpen(currentRefinement)) return ''
|
|
115
|
+
const count = indices[0]?.hits?.length ?? 0
|
|
116
|
+
return count === 0 ? 'No results' : `${count} search result${count === 1 ? '' : 's'}`
|
|
117
|
+
}
|
|
118
|
+
|
|
101
119
|
function onInput(refine, event) {
|
|
102
120
|
open.value = true
|
|
103
121
|
active.value = -1
|
|
@@ -131,6 +149,11 @@ function onFocusOut(event) {
|
|
|
131
149
|
</script>
|
|
132
150
|
```
|
|
133
151
|
|
|
152
|
+
### Match entity schema
|
|
153
|
+
|
|
154
|
+
Adapt the label, placeholder, highlighted attributes, result content, and destination to
|
|
155
|
+
the confirmed indexed entity. `products` and `name` above are examples.
|
|
156
|
+
|
|
134
157
|
Details that break accessibility if changed:
|
|
135
158
|
|
|
136
159
|
- `role="combobox"` on the `input`, not a wrapper — with `aria-controls` at the popup's
|
|
@@ -146,11 +169,42 @@ Details that break accessibility if changed:
|
|
|
146
169
|
top — it fights `@mousedown.prevent`.
|
|
147
170
|
- `.is-active` needs a visible style, not just `aria-selected` — it is a keyboard user's
|
|
148
171
|
only focus feedback, since focus never leaves the input.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
172
|
+
- Pass a stable, unique `id-prefix` whenever this component is rendered, for example
|
|
173
|
+
`id-prefix="catalog-search"`. Use a different value for each simultaneous mount so
|
|
174
|
+
labels and `aria-controls` never point at another instance.
|
|
175
|
+
|
|
176
|
+
Keep the associated label visible when the layout permits; otherwise use the app's
|
|
177
|
+
existing visually-hidden utility rather than removing it. The persistent `aria-live`
|
|
178
|
+
status above announces result counts and no-results state without moving focus; keep it
|
|
179
|
+
in the app's visually-hidden utility class (`.visually-hidden` is a placeholder for
|
|
180
|
+
whatever the project already uses) so it never shifts the layout it sits in. Surface
|
|
181
|
+
loading and errors through the same status region if the surrounding app exposes those
|
|
182
|
+
states.
|
|
183
|
+
|
|
184
|
+
Target WCAG 2.2 AA after applying the app's design tokens:
|
|
185
|
+
|
|
186
|
+
- input, placeholder, and result text clear 4.5:1 contrast;
|
|
187
|
+
- meaningful icons, control boundaries, and focus indicators clear 3:1 contrast;
|
|
188
|
+
- focus is visible and unobscured, and no state relies on color alone;
|
|
189
|
+
- controls have 24 by 24 CSS pixel targets or enough spacing around smaller targets;
|
|
190
|
+
- the experience reflows at a 320 CSS pixel viewport without horizontal page scrolling;
|
|
191
|
+
- any added animation respects `prefers-reduced-motion`.
|
|
192
|
+
|
|
193
|
+
The panel is your own markup with no widget theme to inherit. Give the input and
|
|
194
|
+
`.search-panel` non-transparent surfaces from the app's tokens, and make the input
|
|
195
|
+
boundary distinguishable from its surrounding background in every supported theme. Use
|
|
196
|
+
`position: absolute`, a 320px minimum when space permits, a viewport cap, an input-edge
|
|
197
|
+
anchor, and a scrollable maximum height:
|
|
198
|
+
|
|
199
|
+
```css
|
|
200
|
+
.search-panel {
|
|
201
|
+
min-width: min(320px, calc(100vw - 2rem));
|
|
202
|
+
max-width: calc(100vw - 2rem);
|
|
203
|
+
max-height: min(24rem, calc(100vh - 2rem));
|
|
204
|
+
overflow-y: auto;
|
|
205
|
+
left: 0; /* right: 0; left: auto for an input aligned to the right */
|
|
206
|
+
}
|
|
207
|
+
```
|
|
154
208
|
|
|
155
209
|
`indices` supports federated search across indices; this project has one, so `indices[0]`
|
|
156
210
|
is always the one to read.
|