import * as React from 'react';
import { Button } from '@/components/button';
import { Toast, toast } from '@/components/toast/toast';
import type { ToastKind } from '@/components/toast/toast';
import { FilterField, narrow } from '~/chrome/ui/axis-filter';
import { CopyBlock } from '~/chrome/ui/copy';
import { GalleryPage } from '~/chrome/ui/gallery-page';
import { MatrixGrid } from '~/chrome/ui/matrix';
import { PACKAGE } from '~/chrome/lib/package-name';
import { TOAST_POSITIONS, useToastPosition } from '~/chrome/hooks/use-toast-position';
import { useMessages } from '../i18n';
import { TOAST_KINDS } from '../registry/overlays';
import { useParam } from '../router';
import { FigmaLayoutView, toastCall, toastDemo } from './toasts-figma';
/**
* Every toast the kit can draw, on one page, in two shapes — the same split
* `/buttons`, `/alerts`, `/avatars` and `/badges` use.
*
* Toast is the odd one out among them, and the page is shaped around why: its
* kind is not a prop. You do not render a toast, you call `toast.success(…)`
* and a store hands it to the one `` mounted near the app root. So
* the Gallery comes in two sections that answer two different questions —
* *what does each kind look like* (a static grid) and *how do you actually use
* it* (live, imperative, exactly as a consumer would write it).
*
* **Figma layout** is CBAR's shape — a single column, one row per state, in
* canvas order. It lives in `./toasts-figma` and covers all five variants the
* set declares.
*
* The static grids draw the kit's internal `Toast` with `duration={Infinity}`,
* because nothing in the published surface can put five toasts side by side.
* That is why both grids pass `snippet` rather than letting the copy button
* serialise what it sees: the copied text is the call, never ``.
*/
type Kind = (typeof TOAST_KINDS)[number];
const EVERY_TOKEN: readonly string[] = [...TOAST_KINDS];
const TABS = ['gallery', 'figma'] as const;
export function ToastsPage() {
const m = useMessages();
return (
m.toasts.tabs[name]}
views={{ gallery: , figma: }}
/>
);
}
function GalleryView() {
const m = useMessages();
const [query, setQuery] = useParam('q', '');
const [closeParam, setClose] = useParam('close', '');
const closeButton = closeParam === '1';
const needle = query.trim().toLowerCase();
const nothingMatches = needle.length > 0 && !EVERY_TOKEN.some((token) => token.includes(needle));
const kinds = React.useMemo(() => narrow(TOAST_KINDS, needle), [needle]);
/* Same static path as the Figma tab: `Infinity` skips the dismiss timer
outright, and the `` gives the `
` the list parent axe expects. */
const cell = (kind: Kind) => (
);
return (
{/* Native controls on purpose: a page documenting the kit must not
document it with itself, or a broken component hides its own bug. */}
setQuery(e.target.value)}
placeholder={m.toasts.searchPlaceholder}
className="h-9 min-w-52 flex-1 rounded-md border border-input bg-background px-2 text-sm"
/>
setClose(e.target.checked ? '1' : '')}
className="size-4 rounded border-input"
/>
{nothingMatches ? (
{m.toasts.empty(query)}
) : (
<>
{m.toasts.kindsTitle}
{m.toasts.kindsLead}
cell(row as Kind)}
snippet={(row) => toastCall(row as ToastKind)}
cellTitle={(row) => `kind=${String(row)}`}
copyAll={false}
/>
{m.toasts.liveTitle}
{m.toasts.liveLead}
{m.toasts.position.title}
{m.toasts.position.lead}
>
)}
);
}
/**
* The imperative half — the API a consumer actually writes.
*
* Nothing here pins a toast (`duration: Infinity`) and nothing fires on mount.
* The store is a module singleton, so a pinned toast would follow the visitor
* onto every other page in the showcase; letting them expire on their own is
* both the honest demo and the safe one. No `` either — `main.tsx`
* mounts the only one the showcase needs.
*/
function LiveView() {
const m = useMessages();
return (
);
}
/** Where the bar sits inside its cell — a miniature of the corner it stands for. */
const CELL_ALIGN: Record<(typeof TOAST_POSITIONS)[number], string> = {
'top-left': 'items-start justify-start',
'top-center': 'items-start justify-center',
'top-right': 'items-start justify-end',
'bottom-left': 'items-end justify-start',
'bottom-center': 'items-end justify-center',
'bottom-right': 'items-end justify-end',
};
/**
* The corner, which is the one part of a toast that is not chosen at the call
* site.
*
* `position` belongs to the `Toaster`, so this section writes it into the hash
* and the showcase's own mounted `Toaster` follows — see `chrome/ui/toaster.tsx`
* for why a second one on this page would have duplicated every toast instead
* of moving it. Picking a corner therefore *is* the demo: the button below
* raises a real toast, and it appears where the diagram says it will.
*
* The picker is a native radio group inside a fieldset. Each cell's accessible
* name is the prop value itself (`top-left`), untranslated like every other
* axis value in the showcase, because it is the string a consumer types.
*/
function PositionView() {
const m = useMessages();
const [position, setPosition] = useToastPosition();
const snippet = [
`import { Toaster } from '${PACKAGE}/toast';`,
'',
'/* One per app, mounted near the root. Every toast() renders into it. */',
``,
].join('\n');
return (