/**
* `ViewToggle` + `useViewMode` — the list/card switch every entity screen shares.
*
* The control is a `RadioGroup variant="segmented"` with ICON-ONLY options: each
* option's text label is rendered for assistive tech only (`visuallyHiddenLabel`),
* so the segment shows just its glyph while keeping a real accessible name. A single
* screen's choice persists across sessions in `localStorage`, keyed per SURFACE —
* the tools screen and the flows page each remember their own view — via
* {@link useViewMode}. The two live in the SDK so every entity screen (and the
* flows page) toggles identically; `EntityCardGrid` renders the card half.
*/
import { type ReactNode, useCallback, useState } from 'react';
import { GridIcon, MenuIcon } from './icons';
import { RadioGroup } from './radio-group';
/** The two entity-screen layouts. */
export type ViewMode = 'list' | 'cards';
const OPTIONS = [
{ value: 'list', label: 'List view', icon: , visuallyHiddenLabel: true },
{ value: 'cards', label: 'Card view', icon: , visuallyHiddenLabel: true },
] as const;
export interface ViewToggleProps {
readonly value: ViewMode;
readonly onValueChange: (mode: ViewMode) => void;
/** The control's accessible group name (e.g. "Tools view"). */
readonly 'aria-label': string;
readonly disabled?: boolean;
}
/** The icon-only segmented switch. Controlled: pair it with {@link useViewMode}. */
export function ViewToggle({
value,
onValueChange,
'aria-label': ariaLabel,
disabled,
}: ViewToggleProps): ReactNode {
return (
{
// The option values are a closed pair, so the widening back to `ViewMode`
// is exact — a stray value could only come from a mismatched options list.
onValueChange(next as ViewMode);
}}
disabled={disabled}
/>
);
}
const STORAGE_PREFIX = 'tai-studio.view-mode.';
function readStoredMode(surface: string, fallback: ViewMode): ViewMode {
try {
const raw = globalThis.localStorage.getItem(STORAGE_PREFIX + surface);
if (raw === 'list' || raw === 'cards') return raw;
} catch {
// No storage (private mode / non-browser) — fall back to the caller's default.
}
return fallback;
}
/**
* The persisted view mode for one `surface` (a stable key like `'tools'`). Returns
* the stored choice or `fallback` when none is set, and a setter that writes through
* to `localStorage`. Storage failures degrade to in-memory only — never a thrown
* boot, matching the theme preference's defensive reads/writes.
*/
export function useViewMode(
surface: string,
fallback: ViewMode = 'list',
): readonly [ViewMode, (mode: ViewMode) => void] {
const [mode, setModeState] = useState(() => readStoredMode(surface, fallback));
const setMode = useCallback(
(next: ViewMode) => {
setModeState(next);
try {
globalThis.localStorage.setItem(STORAGE_PREFIX + surface, next);
} catch {
// Storage unavailable — the choice still applies for this session.
}
},
[surface],
);
return [mode, setMode];
}