Feature: Integration page search, a jump-to command palette on /integration As a developer reading the API reference I want to find an endpoint, a body field or an error and jump to it So that I do not have to scroll or use the browser's find-in-page Background: Given the SPA route /integration of the verified back-office (static page, no /v1 call) And search is client-side only, over a static index built from API_GROUPS, SESSION_GROUPS, PUBLIC_AUTH_ROWS, ERROR_ROWS and the page sections, with these fields per endpoint: | field | indexed text | | method | GET, POST, PATCH, DELETE | | path | for example /v1/media/merge | | description | the first sentence of the endpoint description | | access chip | Public, Verified user, Owner or admin, Owner only, Session only | | request field | body field names and query parameter names | | error | each error code and message of the endpoint | And code samples, prose and response examples are never searched And a term matches as a case-insensitive substring; "mdia" does not match "media" And a query is split on spaces and every term must match somewhere in the same endpoint And there are no recent searches: an empty query shows only the "Suggested" group And the search never hides, filters or reorders the page; find-in-page keeps working And the components are Nuxt UI v4 (UModal with a custom listbox, UInput, UButton, UKbd, UBadge, UIcon) and not UCommandPalette, with design tokens only And every count and sample row below follows the real catalogue, not the board's sample data And the palette is the board of docs/design/screens/integration.md "Search (jump-to palette)", states 8a to 8h, in light and dark, at 1440x900 and 390x844 And no state is date dependent Rule: Trigger and shortcuts Scenario: Desktop trigger Given the viewport is 1440x900 Then the page header holds a field-style outline neutral button, 320x36, right of the title block And it shows the icon, the placeholder "Search the API reference…" and the key hint "/" And its aria-label is "Search the API reference" And it has aria-keyshortcuts "/ Control+K Meta+K" And it is not sticky: it scrolls away with the header Scenario: Mobile trigger Given the viewport is 390x844 Then the sticky "Jump to" bar holds a 44x44 icon-only button right of the select And its aria-label is "Search the API reference" And the desktop trigger is not displayed Scenario Outline: Opening the palette with a shortcut Given focus is on When the user presses Then the palette Examples: | focus | keys | result | | the page body | / | opens and the "/" is not typed | | a button | / | opens | | the page body | Ctrl+K | opens | | the page body | Cmd+K | opens | | a text input of the page | / | does not open and "/" is typed | | a textarea | / | does not open | | a text input | Ctrl+K | opens | Scenario: Ctrl+K and Cmd+K replace the browser behaviour on this page When the palette opens with Ctrl+K or Cmd+K Then the default browser action is prevented Scenario: Shortcuts only exist on /integration When the user leaves /integration Then "/", Ctrl+K and Cmd+K no longer open a palette And Ctrl/Cmd+S on the editor route is unchanged Scenario: Opening focuses the input and closing restores focus Given the user opened the palette from the desktop trigger Then focus is in the input When the user closes the palette Then focus returns to the trigger And when it was opened with "/" or Ctrl+K, focus returns to the element that had it Rule: Palette layout Scenario: Desktop modal Given the viewport is 1440x900 When the palette opens Then it is a modal 720 px wide, 96 px from the top, on the existing overlay And the input has placeholder "Search the API reference…" And the footer reads "↑↓ Move", "↵ Open", "Esc Close" and the count Scenario: Mobile full-screen sheet Given the viewport is 390x844 When the palette opens Then it is a full-screen sheet And the input row holds a 44 px "Cancel" button And pressing "Cancel" closes the palette and returns focus to the trigger Rule: Content per state Scenario: 8b Open, empty query When the palette opens with an empty query Then one group "Suggested" shows these 5 rows in order: | row | description | access | | Authentication | Public access and API keys | section | | POST /v1/media/ | Upload an image (multipart). | Verified user | | Errors | Status codes and messages | section | | GET /v1/article/ | List one page of articles, newest update first, with the total and the tab counts (all, online, draft and suggestions). | Public | | PATCH /v1/user/me | Update my profile. | Verified user | And the first row is selected And the footer count and the live region read "5 suggestions" Scenario: 8c Typing "media" When the user types "media" Then group "Endpoints" lists the first 6 of the 16 matching routes (GET /v1/builtin-type/ first, because its description names Media, then the Media routes) And group "Sections" lists "Media" with description "11 routes" And the line "10 more endpoints. Keep typing to narrow the list." follows the rows And the count reads "17 results" And the live region reads "17 results, showing 7" And "media" is highlighted with --rc-sand on --rc-on-sand in paths and descriptions Scenario: 8d More endpoints than the cap When the user types "404" Then group "Endpoints" lists 6 rows, each with a second line "Error" and the match, for example "404 Article not found" And group "Sections" lists "Errors" with description "Status codes and messages" And the line "17 more endpoints. Keep typing to narrow the list." follows the endpoint rows And the count reads "24 results" And the live region reads "24 results, showing 7" Scenario: 8e Match on a body field When the user types "keep_id" Then group "Endpoints" lists exactly POST /v1/media/merge And its description is "Merge identical copies into one image." and its chip is "Verified user" And a second line reads "Body field keep_id" And the count reads "1 result" Scenario: 8f Session-only route When the user types "login" Then group "Endpoints" lists POST /v1/auth/login, "Log in", with a dashed "Session only" chip and a lock icon And its path weight is 400 and its text is --ui-text-muted And group "Sections" lists "Session only" with a lock icon and "37 routes, not available to API keys" And the count reads "2 results" Scenario: 8g No results When the user types "xyz" Then the empty slot reads "No match for "xyz". Try a path, a method or a status code like 404." And the count reads "0 results" And the live region reads "No results" Rule: Matching Scenario Outline: Multi-term and field matching When the user types "" Then the endpoint rows are those whose indexed text contains every term And the second line is "" Examples: | query | example hit | second line | | post media | POST /v1/media/ | none | | PATCH user | PATCH /v1/user/me | none | | keep_id | POST /v1/media/merge | Body field keep_id | | page | GET /v1/site/{key}/sitemap | Query parameter page | | 404 | several routes | Error 404 … | | OWNER ONLY | owner-only routes | none | | mdia | none | 0 results | Scenario: Ordering Given a route matches "token" only in a body field and another matches in its path Then the path match is listed first And routes that tie keep the order of the catalogue Scenario: A row shows the second line only when matched in a field or error Then a row matched in method, path, description or chip has no second line Scenario: Highlighting Then every term occurrence in the label and description is wrapped in a highlight And terms inside the second line are highlighted too Rule: Keyboard navigation Scenario Outline: Moving the selection Given the palette shows 7 rows and row is selected When the user presses Then row is selected and the input keeps focus Examples: | from | key | to | | 1 | ArrowDown | 2 | | 7 | ArrowDown | 1 | | 1 | ArrowUp | 7 | | 3 | Home | 1 | | 3 | End | 7 | Scenario: Typing resets the selection Given row 3 is selected When the user types another character Then row 1 is selected Scenario: Enter opens and Escape closes When the user presses Enter on a selected row Then that row is selected as in the "Selecting a result" rule When the user presses Esc Then the palette closes, nothing scrolls and the hash is unchanged Rule: Accessibility Scenario: Roles and live region Then the input has role "combobox", aria-expanded, aria-controls, aria-autocomplete "list" and aria-activedescendant pointing at the selected row And the list has role "listbox"; each group has role "group" and aria-labelledby its title And each row has role "option", aria-selected and an aria-label with method, path, description and access And a visually hidden polite live region announces the count of the current state Rule: Selecting a result Scenario: 8h Selecting an endpoint Given the palette shows POST /v1/media/merge When the user presses Enter, or clicks the row Then the palette closes And the page scrolls to the card inside its own scroll container, below the sticky offset And the card is expanded And focus is on the card's button And the row flashes: --rc-sand fill and a 2 px --ui-primary ring for 1.5 s, then the open-card fill And the section index and "Jump to" read "Media" (existing scroll-spy) And the URL hash is #ep-, written with history.replaceState so no history entry is added And the other cards keep their open or closed state Scenario: Selecting a section When the user selects the "Errors" row Then the palette closes, the page scrolls to the Errors heading, focus is on the heading, the hash is #errors and nothing is flashed or expanded Scenario: Selecting a session-only route When the user selects POST /v1/auth/login Then the page scrolls to the "Session only" section and focus goes to its heading And no card expands Scenario: Reduced motion Given the user prefers reduced motion When a card is selected Then the flash is a static --rc-sand fill and ring with no fade Scenario: Shared link When /integration#ep- is loaded Then the card is scrolled to, expanded, focused and flashed as above And the palette stays closed Scenario: Unknown hash When /integration#ep-nope is loaded Then the page behaves as today, nothing is expanded and no error shows Scenario: The page is never filtered Given a query is typed Then every section and card of the page stays in the DOM, unchanged