import type { LegendGroup, LegendLayer, LegendState } from './types' const byOrder = (a: T, b: T): number => a.order - b.order /** * Groups sorted by `order`. * * @experimental This API is new and may change in a future release. */ export function selectOrderedGroups(state: LegendState): LegendGroup[] { return Object.values(state.groups).sort(byOrder) } /** * Layers belonging to `groupId` (pass `undefined` for ungrouped, top-level * layers), sorted by `order`. * * @experimental This API is new and may change in a future release. */ export function selectLayersByGroup( state: LegendState, groupId: string | undefined, ): LegendLayer[] { return Object.values(state.layers) .filter((l) => l.groupId === groupId) .sort(byOrder) } /** * All layers sorted by `order`, regardless of group. * * @experimental This API is new and may change in a future release. */ export function selectOrderedLayers(state: LegendState): LegendLayer[] { return Object.values(state.layers).sort(byOrder) } /** * `true` when every group is collapsed (`false` with no groups) — drives the * "Collapse all groups" / "Expand all groups" menu label flip. * * @experimental This API is new and may change in a future release. */ export function selectAllGroupsCollapsed(state: LegendState): boolean { const groups = Object.values(state.groups) return groups.length > 0 && groups.every((g) => g.collapsed) } /** * Whether the legend has groups — gates panel-level group actions. * * @experimental This API is new and may change in a future release. */ export function selectHasGroups(state: LegendState): boolean { return Object.keys(state.groups).length > 0 } /** * Visible layers grouped for the enabled-layers menu. * * @experimental This API is new and may change in a future release. */ export function selectVisibleLayersGrouped( state: LegendState, ): { group?: LegendGroup; layers: LegendLayer[] }[] { const visible = (groupId: string | undefined) => selectLayersByGroup(state, groupId).filter((l) => l.visible) const sections: { group?: LegendGroup; layers: LegendLayer[] }[] = [] for (const group of selectOrderedGroups(state)) { const layers = visible(group.id) if (layers.length > 0) sections.push({ group, layers }) } const ungrouped = visible(undefined) if (ungrouped.length > 0) sections.push({ layers: ungrouped }) return sections } /** * Whether a layer has any collapsible body — something the row's `Collapse` * can actually fold. A layer with no variables, sections, or helper note * renders an empty body, so there is nothing to collapse. Gates the row's * title-area collapse affordance (chevron + click). * * @experimental This API is new and may change in a future release. */ export function selectLayerCollapsible( state: LegendState, layerId: string, ): boolean { const layer = state.layers[layerId] if (!layer) return false return ( layer.variables.length > 0 || (layer.sections?.length ?? 0) > 0 || // Truthiness matches the render condition — an empty-string note renders // no footer, so it must not enable the collapse affordance. Boolean(layer.helperText) ) } /** * Tri-state visibility — derived for groups from their member layers, and the * state contract of a controlled `Legend.VisibilityToggle` for any custom * element kind. */ export type LegendVisibility = 'visible' | 'hidden' | 'mixed' /** * Tri-state group visibility: `'visible'` when every member layer is visible * (also for empty groups), `'hidden'` when none is, `'mixed'` otherwise. * * @experimental This API is new and may change in a future release. */ export function selectGroupVisibility( state: LegendState, groupId: string, ): LegendVisibility { let anyVisible = false let anyHidden = false for (const layer of selectLayersByGroup(state, groupId)) { if (layer.visible) anyVisible = true else anyHidden = true } if (anyVisible && anyHidden) return 'mixed' return anyHidden ? 'hidden' : 'visible' }