Library toolbar
The row above a library grid: the scope switch on the left and its filter menus on the right.
import { FolderFilter, Toolbar } from "@/pages/library/LibraryPage";
import ComposerMenu from "@/pages/harness/ComposerMenu";
import "@/pages/library/library.css";The toolbar sits above every grid on the Components and Templates tabs of the Library. It says what is shown on the left (the scope switch, "My workspace" or "Library", with a count) and offers filters as menus on the right. It is made of three pieces:
Toolbar, a row with aleadslot on the left and its children pushed to the right.- The scope switch, which is markup the Library page builds and passes as
lead. It is not a component. FolderFilter, a Folder menu plus a chip for the tag picked on a card. Other filters, such as Status and Type, are composer menus.
In use
The workspace scope has Folder and Status filters. Switch to Library and it has only Type, because library components have no folder or status yet.
const lead = (
<div className="lib-scope" role="group" aria-label="Show">
{scopes.map((s) => (
<button
key={s.id}
type="button"
aria-pressed={scope === s.id}
className={`lib-scope__option${scope === s.id ? " lib-scope__option--active" : ""}`}
onClick={() => setScope(s.id)}
>
{s.label}
<span className="lib-scope__count">{s.count}</span>
</button>
))}
</div>
);
<Toolbar lead={lead}>
<FolderFilter folders={folders} list={items} folder={folder} onFolder={setFolder} tag={tag} onTag={setTag} />
<ComposerMenu
label="Status"
value={filter}
options={STATUS_FILTERS.map((f) => ({ id: f.id, label: `${f.label} ${count(f.id)}` }))}
onChange={setFilter}
/>
</Toolbar>Each menu option carries its count in the label ("Demo campaigns 4", "Draft 1"), and the "All" option counts everything in list.
A tag picked on a card
Tags are picked by clicking one on a card (see Filing). While a tag is active, FolderFilter shows it as a blue chip before the Folder menu. Clicking the chip clears it.
<FolderFilter folders={folders} list={items} folder={folder} onFolder={setFolder} tag="demo" onTag={setTag} />Scope only
When the workspace is empty there is nothing to filter. The tab still renders the toolbar with just the scope switch, so people can move to Library from the same place.
<Toolbar lead={lead} />
<Empty icon={Shapes} title="Your workspace has no components yet" … />FolderFilter also renders nothing when folders is empty and no tag is picked.
Other uses of the scope switch
The same .lib-scope classes draw two more segmented controls:
.lib-scope--fillstretches the options to the full width. Page settings uses it for "Public" and "Anyone with the password"..lib-scope--viewputs the switch on a--color-grey-100track. The page builder uses it for Preview and Code, withrole="tablist"androle="tab"instead ofaria-pressed.
<div className="lib-scope lib-scope--fill" role="group" aria-label="Restrict access">
{[["public", "Public"], ["password", "Anyone with the password"]].map(([id, label]) => (
<button key={id} type="button" aria-pressed={access === id} className={`lib-scope__option${access === id ? " lib-scope__option--active" : ""}`} onClick={() => setAccess(id)}>
{label}
</button>
))}
</div>Anatomy
| Part | Class | Style |
|---|---|---|
| Row | .lib-sort | Flex row, items centred, space between, 12px gap. |
| Filters | .lib-sort__filters | Flex row with an 8px gap, pushed right with margin-left: auto. |
| Scope track | .lib-scope | 2px padding, 8px corners, --color-grey-50 fill. |
| Scope option | .lib-scope__option | 12px text in --color-grey-500, 5px by 12px padding, 6px corners. Hover turns the text to the primary text colour. |
| Active option | .lib-scope__option--active | White fill, 1px --color-grey-100 border, a 1px shadow, medium weight. |
| Count | .lib-scope__count | Tabular figures in the muted text colour (#8E93AF). |
| Tag chip | .lib-tag.lib-tag--on | 11px text on --color-primary-50, --color-primary-300 border, --color-primary-700 text, fully rounded. |
| Folder menu | ComposerMenu | A secondaryGhost Button at md (32px), label then value in medium weight, then a caret. |
Props
Toolbar
| Prop | Type | Default | Notes |
|---|---|---|---|
lead | ReactNode | Left side. The scope switch on the Library page. | |
children | ReactNode | Right side. Filter menus and the tag chip. |
FolderFilter
| Prop | Type | Default | Notes |
|---|---|---|---|
folders | string[] | Folder names in use. The Library page gets them from foldersOf(items) in useLibraryStore.js. | |
list | { folder }[] | Everything that can be filtered, used for the counts in the menu. | |
folder | string | null | The picked folder. null shows "All". | |
onFolder | (folder: string | null) => void | Called with null when "All" is picked. | |
tag | string | null | The tag picked on a card. Shown as a chip when set. | |
onTag | (tag: null) => void | Called with null when the chip is clicked. |
All six are needed. There are no defaults.
Accessibility
- The scope options are
<button>elements witharia-pressed, grouped byrole="group"andaria-label="Show". Screen readers announce each as a toggle button. - The focus ring on a scope option is a 2px
--color-primary-300outline inset by 2px. - The menus are product dropdown menus: arrow keys move through the options, Enter picks one, Escape closes.
- The tag chip reads "Tagged demo ×". The "×" is text, so some screen readers say "times". It has no
aria-labelthat says it clears the tag.
Do and don't
- Do lead with the scope, then filters on the right, as in menus and filters.
- Do put counts in the scope and in the menu options, so people can see what a filter will leave before they pick it.
- Do pair a filtered-out grid with the "No components match these filters" empty state and a "Clear filters" action.
- Don't add a third scope option. If more than two lists are needed, they belong in tabs.
- Don't use the scope switch for settings that change data. It only changes what is shown. Page settings is the one exception, through
.lib-scope--fill.
When to use it
Use Toolbar and FolderFilter for grids of library items. For a two-option switch elsewhere in the product, use TabToggle from @/ui; the .lib-scope classes are local to the library.
Where it is used
| File | What |
|---|---|
src/pages/library/LibraryPage.jsx, LibraryPage | Builds the scope switch with counts and passes it as lead. |
src/pages/library/LibraryPage.jsx, WorkspaceComponents | Toolbar with FolderFilter and a Status menu. |
src/pages/library/LibraryPage.jsx, LibraryComponents | Toolbar with a Type menu. |
src/pages/library/LibraryPage.jsx, Templates | Toolbar with FolderFilter in the workspace scope, and nothing on the right in the library scope. |
src/pages/library/PageSettings.jsx | .lib-scope--fill for access control. |
src/pages/library/PageBuilderPage.jsx | .lib-scope--view for Preview and Code. |