// Family renderer: one family's presets as a uniform tile grid. Every family // renders through this; the per-preset tile decides wallpaper-vs-recessed by // whether it has a resolved thumbnail, so a family with no thumbnails just // renders recessed buttons of the same footprint. The tile renderer is the // `renderTile` slot or the default. // // The thumbnail URI comes from the platform-split `resolveImageUri`: the // source URL on web, the in-bundle file:// URI on native; undefined for a preset // with no thumbnail (the tile then renders its recessed variant). import { Fragment } from 'react'; import { type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'; import { presetTileTestId } from '../../lib/test-id'; import { PresetTile } from '../preset-tile'; import type { PresetView, RenderTile } from './preset-book-menu.types'; import { resolveImageUri } from './resolve-image-uri'; interface PresetGridProps { readonly presets: ReadonlyArray; readonly value: string | null; readonly onSelect: (id: string | null) => void; readonly disabled?: boolean | undefined; /** Override the tile rendering (BYO tile, e.g. to add a badge). */ readonly renderTile?: RenderTile | undefined; /** NativeWind class for the grid container; resolved via the `./nativewind` interop. */ readonly className?: string | undefined; readonly style?: StyleProp | undefined; } const defaultRenderTile: RenderTile = (preset, state) => ( ); export function PresetGrid(props: PresetGridProps) { const { presets, value, onSelect, disabled = false, renderTile, style } = props; const renderItem = renderTile ?? defaultRenderTile; // Resolve each preset's thumbnail URI; pure in (id, source), so React Compiler // memoizes the map by `presets` when the library is compiled. // The native resolver looks the thumbnail up by id in Bundle.main. For image // presets the preset id and the thumbnail's bundle filename coincide (a // background's image id IS its preset id); for composites they differ (the // composite's thumb is bundled as `-thumb.webp`), so pass the // source as the lookup key when it is a string and falls back to the preset // id otherwise. const uriById = new Map( presets.map((p) => { const lookupId = typeof p.source === 'string' ? p.source : p.id; return [p.id, resolveImageUri(lookupId, p.source)] as const; }), ); return ( {presets.map((preset) => { const selected = value === preset.id; return ( {renderItem(preset, { selected, disabled, uri: uriById.get(preset.id), onPress: () => onSelect(selected ? null : preset.id), testID: presetTileTestId(preset.id), })} ); })} ); } const styles = StyleSheet.create({ grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, width: '100%' }, });