- Single source of truth:
tabandpathlive in the URL so refresh/back/forward/share work;selectionandpanelstay in memory to avoid leaking transient UI state into the URL. - Tight API surface:
useLeftAsideexposes state, actions, and helpers in one place—no need to juggle multiple contexts. - Composable with data layers: helpers provide stable query keys for React Query or other caches; actions are designed to be directly wired to event handlers.
tab: string: active tab.path: PathSegment[]: hierarchical path, persisted per tab aspath.<tab>query param (default[]).selection: { entity: Entity | null; panelOpen: boolean }: in-memory selection and detail panel state.- Provider config (
LeftAsideProviderProps):tabs: readonly string[](required),defaultTabtabKey,pathKey(defaults:tab/path, stored aspath.<tab>)initialPathpathCodec(custom parse/serialize)autoOpenPanel(default true),panelEnabled(default true)resetSelectionOnTabChange(default true),resetPathOnTabChange(default false)
const { tab, path, selection, actions, helpers } = useLeftAside<
PathSegment,
Entity
>();
selection.entity; // current selection or null
selection.panelOpen; // panel open flag (auto-false when panel disabled)
actions.setTab(next, { replace, resetPath, resetSelection });
actions.setPath(segments, { replace });
actions.pushPath(segment, { replace });
actions.popPath({ replace });
actions.popTo(index, { replace });
actions.clearPath({ replace });
actions.setSelection(entityOrNull, { openPanel });
actions.clearSelection();
actions.openPanel();
actions.closePanel();
actions.togglePanel();
helpers.segmentAt(index); // PathSegment | undefined
helpers.queryKey(...suffix); // ['left-aside', tab, ...path, ...suffix]export function Layout({
children,
aside,
}: {
children: React.ReactNode;
aside: React.ReactNode;
}) {
return (
<LeftAsideProvider
tabs={["sources", "assets", "queries"]}
defaultTab="sources"
>
<Layout aside={aside}>{children}</Layout>
</LeftAsideProvider>
);
}
const Shell = ({
children,
aside,
}: {
children: React.ReactNode;
aside: React.ReactNode;
}) => {
const { tab, selection, actions } = useLeftAside<string, Entity>();
const { panelOpen, entity } = selection;
return (
<>
<Tabs value={tab} onValueChange={actions.setTab} />
<LeftAsideLayout
asideContent={aside}
detailPanel={entity ? <Detail entity={entity} /> : null}
panelOpen={panelOpen}
/>
{children}
</>
);
};const { path, helpers, actions } = useLeftAside<string, Table>();
const sourceId = path[0];
const tableName = path[1];
const tablesQuery = useQuery({
queryKey: helpers.queryKey("tables", sourceId),
queryFn: () => fetchTables(sourceId),
enabled: Boolean(sourceId),
});
const schemaQuery = useQuery({
queryKey: helpers.queryKey("schema", sourceId, tableName),
queryFn: () => fetchSchema(sourceId, tableName),
enabled: Boolean(sourceId && tableName),
});
const handleSelectSource = (id: string) =>
actions.setPath([id], { replace: true });
const handleSelectTable = (table: Table) => {
actions.setPath([table.sourceId, table.name], { replace: true });
actions.setSelection(table);
};actions.setSelection(entity, { openPanel }): set selection and optionally open panel (defaults toautoOpenPanel).actions.clearSelection(): clear selection and close panel.- With
panelEnabled=false,selection.panelOpenis alwaysfalsebutselection.entityremains available for layout logic.
tabis stored in a query param (defaulttab).pathis stored aspath.<tab>—each tab keeps its own history. By default, switching tabs preserves each tab’s path (resetPathOnTabChangetoggles this).selectionandpanelOpenare not persisted; refresh retains tab/path but not the selected entity.
- Follow project Tailwind rules (no dynamic template literals); continue using
cnfor class composition. - Avoid caching
actions/helpersin refs; always read fromuseLeftAsideto prevent stale references.
- Selection not persisted: keeps transient UI out of sharable URLs. If you need it persisted, write
selection.entity.idinto a query param at the call site and hydrate viainitialPath/setSelection. - Query key convention:
helpers.queryKeyprefixes with['left-aside', tab, ...path]for consistent caching. If you want stricter isolation, append your own suffix. - Panel behavior:
autoOpenPanelis click-to-open by default. For explicit “expand” UX, disableautoOpenPaneland callactions.openPanelfrom the UI.
- Tabs/paths: use
setTab/setPath; don’t hand-roll URL query updates. - Selection/panel: use
setSelection/clearSelection; don’t manually sync panel state. - React Query: drive
queryKeyfromhelpers.queryKeyorpath/selection; gate fetches withenabled. - Anti-stale: avoid storing
actions/helpersin refs; read fresh per render.