Search, filters and menus

The command search, the list filter, the language selector, the help card and the toast, each mounted with its keyboard behaviour.

Updated

The shell finds things in three ways: the search palette over every page, document, heading and slide; the filter at the head of a list; and the index panel of every page on the site. The help card lists the keys, and the toast confirms an action in one line. Each plate mounts the real component. The palette, the help card and the toast are fixed to the window in the shell, so each plate's stage is a containing block and they open inside it. The help card and the index panel read the shell's state, so their plates mount them in the small provider described on Controls. The language selector comes from the production mirror of the live site.

The search palette

The trigger is the toolbar's field-shaped button: the search glyph, the word Search and a key chip that reads ⌘K, or Ctrl K on Windows and Linux. It opens a card over a scrim with a 34px filter, a count and the results as ruled rows in groups, each row a glyph, a title and its address. Trigger switches to the labeled tool button the direction corner stacks.

The index covers pages, documents and their headings, directions, archive versions, brand sections, library anchors and the deck's slides. It is a chunk of its own, about 59K, loaded on the first open or as soon as the pointer or focus reaches the trigger. An empty query shows a short map of the site that fits the card without a scroll region. A plain click on a row opens it, and a click with a modifier keeps the browser's meaning. A row is a real link, so opening one leaves this page.

Import and props

import { openSearch, Search } from '@/components/viewer/Search';

<Search />;
openSearch(); // opens the mounted palette from a key owner
PropTypeWhat it does
trigger'field' or 'tool'The toolbar's field-shaped button, or a labeled tool button. Default 'field'.
classNamestringExtra classes on the overlay.
onOpen() => voidCalled as the palette opens, from the button or from a key.

openSearch() fires one document event that every mounted palette answers, so the key owner never holds the palette's state. preloadSearch() starts loading the index ahead of the first open.

Keys

KeyAction
Cmd K, Ctrl KOpens the palette in the shell (on this page, / while focus is inside the plate)
Up, Down arrowsMove the active row
Home, EndFirst and last row, while the query is empty
EnterOpens the active row
EscCloses the palette

The pointer makes the row under it the active one.

Reduced motion and themes

The scrim fades in over 160ms and the card fades and rises 8px over 200ms; under reduced motion both appear at once. The card is paper with a --pt-hair frame in both themes. At 900px and below the addresses leave the rows, and at 600px and below the key line at the foot leaves too.

The list filter

SidebarFilter is the filter row at the head of a list: the field with the search glyph, a clear button while it holds text, and the count at the row's right end. It is controlled, so the host owns the query and the keys. The plate filters the docs pages by title and address. Size sets the field at the index panel's 34px and 13.5px or the sidebar's 30px and 13px.

In the sidebar, Enter opens the first match. On the plate, Enter marks the first match and moves focus to it, and Enter on a focused row follows its link. The clear button empties the field and puts focus back in it.

Import and props

import { SidebarFilter } from '@/components/viewer/SidebarFilter';

<SidebarFilter value={query} onChange={setQuery} onKeyDown={onKey} placeholder='Filter pages' count='14 pages' inputRef={input} />;
PropTypeWhat it does
valuestringThe query.
onChange(value) => voidCalled on every edit and on clear.
onKeyDownkeyboard handlerThe host's keys.
placeholderstringThe field's placeholder, as in Filter pages.
labelstringThe accessible name; the placeholder when absent.
countstringThe count at the row's right end, as in 14 pages.
inputRefrefThe input, for the host's focus moves.
classNamestringExtra classes on the row.

--pt-filter-h and --pt-filter-fs, set on .pt-filter, size the field.

Keys

These are the sidebar's keys, which the plate's host repeats.

KeyAction
EscClears the query; with the field empty, lets go of focus
Down arrowMoves focus to the first row
EnterTakes the first match while the field holds text
Up, Down arrows on a rowWalk the rows; Up on the first row returns to the field
Home, End on a rowFirst and last row

States

StateWhat draws it
RestA 1px --pt-hair frame, the glyph and placeholder in titanium
FocusThe frame and the glyph in ink
TextThe clear button inside the field, 4px from its end

The language selector

The live site's footer seats its language selector at the right end of its last strip. The trigger is the language glyph, the current language's name and a chevron, and it opens a popover of the site's eight locales above itself. The plate mounts the production mirror's rebuild, which is a stub: choosing a language closes the popover and changes nothing.

The popover's right edge sits on the trigger's right edge, 4px above it. Where that would put its left edge inside 16px of the window's edge, as on a phone, it moves right. Each flag is a flag-icons sprite at 16px, and the active row carries a check.

Import and props

import LanguageSelector from '@/app/d/production/sections/LanguageSelector';

<div className='toolchain-root prod-root'>
  <LanguageSelector />
</div>;

The selector takes no props. Its rules live in prod-chrome.css under .prod-root, and its colors under .toolchain-root.prod-root, so the host carries both classes. The live site's selector is LanguageSelector.tsx in gt-cloud's shared UI package.

Keys

KeyAction
Enter, SpaceOpens the popover from the trigger
TabWalks the rows
Enter, Space on a rowPicks it and closes the popover
EscCloses the popover and returns focus to the trigger

A pointer press outside the selector also closes it. The mirror's rows are radios in a radio group with no arrow-key movement, so Tab walks them.

The help card and the toast

HelpCard lists the keys of the route it opens on. Its rows come from the shell's one key table (shellKeyRows), grouped as Move, View, Panels and Theme, so Route switches between a paged route and a flow route and the card follows: the paging rows and the view letters appear only where they act. Toast is the one-line notice over everything; Say shows it for its hold, and a second Say restarts the timer.

A click anywhere on the card closes it. The key owner handles Escape: in the shell that is useShellKeys, and on the plate it is the plate's own handler.

Import and props

import { HelpCard } from '@/components/viewer/HelpCard';
import { Toast, useToast } from '@/components/viewer/Toast';

const toast = useToast();
<HelpCard />;
<Toast message={toast.message} on={toast.on} />;
toast.say('Link to slide 12 copied');
ExportPropsWhat it does
HelpCardrows?, note?The card for the shell's route. rows replaces the derived rows and note the footnote; an empty note hides it.
Toastmessage, onThe notice. It keeps its text through the fade and is a polite live region.
useToast(hold?)hold in ms, default 1400Returns { message, on, say }; say(msg, hold?) shows a message and restarts the timer.

Inside the shell, components call say() from usePtShell() and never mount a toast of their own.

Keys

KeyAction
?Opens and closes the card (on this page, while focus is inside the plate)
EscCloses the card

Reduced motion and themes

The card's scrim fades over 160ms and the card fades and rises 8px over 200ms. On close the card stays mounted for 160ms so both can fade back out. The toast fades and rises 4px over 160ms, centered 22px above the bottom edge, and at 600px and below it hangs 8px under the toolbar. Under reduced motion none of them moves. The toast is paper text on an ink plate with a 1px paper outline, so it separates from dark content in either theme.

The index panel

IndexPanel is a 460px column that slides in from the right over a scrimmed stage, under the toolbar. It lists one of two sets from src/lib/surfaces.ts in groups, each row a 96 by 54 preview, a name, an address and a line of description, with a switch between the pages on this site and the public places General Translation is live. Opens on picks the set the panel starts on.

The panel builds its rows on its first open, so a route where it stays closed computes nothing. It focuses its filter on every open at desktop width. The filter is a case-insensitive match on a row's text and address, and a group with no match hides. A row on this site opens in place, and a public place opens in a new tab, so a row on the plate leaves this page.

Import and props

import { IndexPanel } from '@/components/viewer/IndexPanel';

<IndexPanel set='site' />;
PropTypeWhat it does
set'site' or 'public'The set the panel opens on.
refrefThe aside, for the shell's measure of the panel's width.

The panel reads panelOpen and setPanel from the shell. The shell draws the scrim (.pt-panel-scrim) inside the stage, so a click on it closes the panel.

Keys

KeyAction
ROpens and closes the panel (on this page, while focus is inside the plate)
Enter in the filterOpens the first match
EscCloses the panel and lets go of focus
TabWalks the set switch, the filter and the rows

Reduced motion and themes

The panel and its scrim move over 180ms; under reduced motion the panel appears in place. Each preview shows its light or dark capture with the theme, and a row without a capture shows its initial on a blank plate.

On this page