/// import { type Cell, cell, Default, handler, lift, recipe, str, } from "commontools"; interface VariantInput { mode?: string; src?: string; alt?: string; } interface VariantDetails { mode: string; src: string; alt: string; } type VariantMap = Record; const DEFAULT_MODES: string[] = ["mobile", "tablet", "desktop"]; const sanitizeMode = (value: unknown): string | undefined => { if (typeof value !== "string") return undefined; const trimmed = value.trim().toLowerCase(); return trimmed.length > 0 ? trimmed : undefined; }; const defaultVariantForMode = (mode: string): VariantDetails => ({ mode, src: `${mode}-placeholder.png`, alt: `Image for ${mode}`, }); const sanitizeModes = (value: unknown): string[] => { const fallback = [...DEFAULT_MODES].sort((a, b) => a.localeCompare(b)); if (!Array.isArray(value)) return fallback; const collected = new Set(); for (const entry of value) { const mode = sanitizeMode(entry); if (mode) collected.add(mode); } const result = [...collected].sort((a, b) => a.localeCompare(b)); return result.length > 0 ? result : fallback; }; const sanitizeSource = ( value: unknown, fallback: string, mode: string, ): string => { if (typeof value === "string") { const trimmed = value.trim(); if (trimmed.length > 0) return trimmed; } return fallback.length > 0 ? fallback : defaultVariantForMode(mode).src; }; const sanitizeAlt = ( value: unknown, fallback: string, mode: string, ): string => { if (typeof value === "string") { const trimmed = value.trim(); if (trimmed.length > 0) return trimmed; } return fallback.length > 0 ? fallback : defaultVariantForMode(mode).alt; }; const sanitizeVariants = ( entries: unknown, modeList: string[], ): VariantMap => { const base: VariantMap = {}; for (const mode of modeList) { base[mode] = { ...defaultVariantForMode(mode) }; } if (!Array.isArray(entries)) return base; for (const entry of entries) { if (!entry || typeof entry !== "object") continue; const record = entry as VariantInput; const mode = sanitizeMode(record.mode); if (!mode || !modeList.includes(mode)) continue; const fallback = base[mode]; const src = sanitizeSource(record.src, fallback.src, mode); const alt = sanitizeAlt(record.alt, fallback.alt, mode); base[mode] = { mode, src, alt }; } return base; }; interface ImageGalleryVariantArgs { modes: Default; variants: Default; activeMode: Default; } const selectDeviceMode = handler( ( event: { mode?: string } | undefined, context: { activeMode: Cell; modes: Cell; history: Cell; }, ) => { const modes = sanitizeModes(context.modes.get()); if (modes.length === 0) return; const requested = sanitizeMode(event?.mode); const next = requested && modes.includes(requested) ? requested : modes[0]; context.activeMode.set(next); const currentHistory = Array.isArray(context.history.get()) ? context.history.get() : []; context.history.set([...currentHistory, next]); }, ); const updateDeviceVariant = handler( ( event: { mode?: string; src?: string; alt?: string } | undefined, context: { variants: Cell; modes: Cell; }, ) => { const modes = sanitizeModes(context.modes.get()); if (modes.length === 0) return; const mode = sanitizeMode(event?.mode) ?? modes[0]; if (!modes.includes(mode)) return; const existing = Array.isArray(context.variants.get()) ? context.variants.get() : []; const sanitized = sanitizeVariants(existing, modes); const current = sanitized[mode] ?? defaultVariantForMode(mode); const src = sanitizeSource(event?.src, current.src, mode); const alt = sanitizeAlt(event?.alt, current.alt, mode); sanitized[mode] = { mode, src, alt }; const nextList = modes.map((entry) => ({ ...sanitized[entry] })); context.variants.set(nextList); }, ); export const imageGalleryVariant = recipe( "Image Gallery Variant", ({ modes, variants, activeMode }) => { const selectionHistory = cell([]); const sanitizedModes = lift((value: unknown) => sanitizeModes(value))( modes, ); const sanitizedVariants = lift((input: { entries: VariantInput[] | undefined; modeList: string[]; }) => sanitizeVariants(input.entries, input.modeList))({ entries: variants, modeList: sanitizedModes, }); const availableVariants = lift((input: { variants: VariantMap; modes: string[]; }) => input.modes.map((mode) => ({ ...input.variants[mode] })))( { variants: sanitizedVariants, modes: sanitizedModes }, ); const selectedMode = lift((input: { candidate: string | undefined; modes: string[]; }) => { const list = input.modes; if (list.length === 0) return "desktop"; const mode = sanitizeMode(input.candidate); return mode && list.includes(mode) ? mode : list[0]; })({ candidate: activeMode, modes: sanitizedModes }); const currentVariant = lift((input: { variants: VariantMap; mode: string; modes: string[]; }) => { const list = input.modes; if (list.length === 0) return defaultVariantForMode("desktop"); const target = input.variants[input.mode]; if (target) return { ...target }; const fallback = list[0]; return { ...input.variants[fallback] }; })({ variants: sanitizedVariants, mode: selectedMode, modes: sanitizedModes, }); const currentSource = lift((variant: VariantDetails) => variant.src)( currentVariant, ); const currentAlt = lift((variant: VariantDetails) => variant.alt)( currentVariant, ); const variantSummary = lift((variant: VariantDetails) => `${variant.mode}:${variant.src}` )(currentVariant); const historyView = lift((value: string[] | undefined) => Array.isArray(value) ? value : [] )(selectionHistory); return { availableModes: sanitizedModes, variantMap: sanitizedVariants, variantList: availableVariants, activeMode: selectedMode, currentVariant, currentSource, currentAlt, variantSummary, label: str`Mode ${selectedMode} uses ${currentSource}`, history: historyView, selectMode: selectDeviceMode({ activeMode, modes, history: selectionHistory, }), updateVariant: updateDeviceVariant({ variants, modes }), }; }, );