Components

SavedViews

A saved-view switcher — Attio's "All Companies ▾" menu.

A generic saved-view switcher: a trigger chip (the active view's own icon + name, or your default label when nothing is active) opens a popover on a "Search views..." box, then every view as a role="option" row. Each row reveals a kebab on hover with Rename / Duplicate / Delete (and Add to favorites, when you pass onToggleFavorite), optionally followed by a "Created by / date" footer, plus a trailing Create new view row. It has no object/table concept of its own — pass whatever SavedView[] your app's own views are.

Available since@crisp-ui-kit/crisp@0.54.0

Installation

npm install @crisp-ui-kit/crisp

Usage

import { SavedViews, type SavedView } from "@crisp-ui-kit/crisp";

const views: SavedView[] = [
  { id: "all", name: "All Companies", icon: <Building2 color="#00D17E" /> },
];

<SavedViews
  label="All Companies"
  views={views}
  activeId={activeId}
  onSelect={setActiveId}
  onCreate={createView}
  onRename={renameView}
  onDuplicate={duplicateView}
  onDelete={deleteView}
/>;

Props

Prop

Type

SavedViewsSaveBar

The "unsaved changes" bar for a currently loaded saved view — Attio's own "Discard changes"

  • a split "Save for everyone ▾" button, measured live. Not part of SavedViews' own trigger/panel: Attio renders this in the page toolbar's trailing slot (TableToolbar's children), replacing whatever sits there at rest (Import/Export, "+ New …"). Swap it in once your own dirty check (live filters/sort/columns vs. the loaded view's snapshot) is true, and back out once it's false — this component carries no dirty state of its own. onSaveAsNew is optional; omit it to render a plain button with no chevron/menu.

Prop

Type

Default view: Discard / Save for everyone

SavedViewsSaveBar has no dirty logic of its own: you decide when it shows. isViewDirty(live, saved) is that check. It deep-compares two { filters, sorts, shownKeys, columnOrder, ...anything else you store } objects and returns true when they differ. It ignores ToolbarFilter.id (the toolbar generates ids per mount, so a plain JSON.stringify compare of an untouched view is always dirty) and treats a missing key, undefined, null and [] as the same. Array order still counts, and so does every other filter field, including join and group.

The default view is the one no one has saved yet: keep its saved snapshot in state, let the toolbar edit the live copy, and show the bar while the two differ. onSave persists the live state as the new default (for everyone); onDiscard resets the live state to the snapshot.

SavedView = { id, name, icon?, shared?, keywords?, createdBy?, createdAt? } — icon is the view's own 16×16 leading icon (coloured, per view); shared renders a small shared-view indicator next to the name; keywords is matched against the search box when name isn't a plain string; createdBy/createdAt render as the kebab menu's own "Created by / date" footer (each independently optional — the footer renders only when at least one is given).

Behavior

  • onToggleFavorite is optional — omit it and each row's kebab menu has no "Add to favorites" item.
  • Search: the panel always opens on a "Search views..." box (real focus moves there, not the trigger) — typing filters the view list by name (or keywords, for a non-string name); a query matching nothing shows "No results", and the trailing "Create new view" row always stays regardless of the filter.
  • Keyboard: with the panel open, focus lives on the search box — ↑/↓ move a virtual highlight over the (possibly filtered) list, Enter chooses it, Escape closes — a still-open kebab menu closes first, the panel closes on the next Escape.
  • A row's kebab click never also selects the row (it stops the click from bubbling into the row's own handler).
  • The trigger's own chrome is white with a soft 3-layer shadow, not a flat grey fill (#772, #784, re-measured live in the trigger's actual view-bar position): elevation.field's exact shadow (inset 0 0 0 1px transparent, 0 0 2px rgba(28,40,64,.18), 0 1px 3px rgba(0,0,0,.04)) over bgSurface/white — the same raised chrome TableToolbar's View settings and Import/Export triggers carry. Supersedes the original #555 measurement (a flat bgRaised grey), which was never re-checked once the trigger moved into its real position next to View settings.
  • Attio's view bar is SavedViews plus TableToolbar's viewSettingsRef/importExportRef (#766, #772, measured live): the view switcher and View settings share the LEFT of one row; Import/Export and a primary action share its RIGHT — the two pairs sit far apart, not next to each other — with a separate row below holding only Sort and Filter. SavedViews doesn't render those other three controls itself — it stays a standalone switcher, unaware of TableToolbar — pair them by rendering two separate <span>s (one next to SavedViews' own trigger, one next to your own primary action) and passing each to TableToolbar as viewSettingsRef/importExportRef; see TableToolbar's own docs for the full composition.

On this page