import { useMemo } from "react"; const toBoolean = (value: unknown, fallback: boolean): boolean => { if (typeof value === "boolean") return value; if (typeof value === "number") return value !== 0; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (["1", "true", "yes", "on"].includes(normalized)) return true; if (["0", "false", "no", "off", ""].includes(normalized)) return false; } return fallback; }; const clampInt = ( value: unknown, min: number, max: number, fallback: number, ): number => { const candidate = Number(value); if (!Number.isFinite(candidate)) return fallback; return Math.max(min, Math.min(max, Math.round(candidate))); }; const clampPadding = (value: unknown, fallback: number): number => clampInt(value, 0, 48, fallback); const HEX_COLOR = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/; const hexColor = (value: unknown, fallback: string): string => typeof value === "string" && HEX_COLOR.test(value.trim()) ? value.trim() : fallback; const ONE_OF = ( value: unknown, allowed: readonly T[], fallback: T, ): T => { const normalized = String(value ?? "") .trim() .toLowerCase(); return (allowed as readonly string[]).includes(normalized) ? (normalized as T) : fallback; }; // Shared swatch layouts (all five). Slider renders via SliderLayout. const LAYOUTS = ["wrap", "grid", "flow", "pill", "slider"] as const; // Flow justify/align values passed raw to CSS justify-content / align-items. const FLOW_JUSTIFY = ["start", "center", "end", "space-between"] as const; const FLOW_ALIGN = ["start", "center", "end"] as const; const DUAL_COLOR_CUTS = [ "vertical", "horizontal", "diagonal", "diagonal_reverse", ] as const; const DUAL_COLOR_GRADIENTS = ["none", "gentle", "aggressive"] as const; const IMAGE_FITS = ["cover", "contain"] as const; // Single-product out-of-stock behaviors. Other contexts use separate keys. const OUT_OF_STOCK_BEHAVIORS = ["cross", "blur", "hide"] as const; // Shape + selected-style enums. The selector-style enum keeps the existing // "underline"/"outline" values and adds "background". const SHAPES = [ "square", "soft", "rounded", "squircle", "circle", "diamond", "hexagon", ] as const; const SWATCH_STYLES = ["underline", "outline", "background"] as const; // Tooltip positions (free contract). const TOOLTIP_POSITIONS = ["top", "bottom", "left", "right"] as const; // Tooltip animation enum. Must match the allow-list in Sanitizer/Normalizer and // FreeTierPolicy exactly. const TOOLTIP_ANIMATIONS = [ "none", "fade", "scale", "shift", "slide", "flip", "zoom", "swing", "elastic", "blur", "rotate", "stretch", ] as const; export function useSwatchSettings( settings: Record, viewportWidth = 1024, ) { return useMemo(() => { const swatchWidth = clampInt(settings.swatch_width, 24, 60, 32); const swatchHeight = clampInt(settings.swatch_height, 24, 60, 32); const mobile = toBoolean(settings.use_mobile_layout, false) && viewportWidth < clampInt(settings.mobile_cutoff, 320, 1200, 768); const layoutSettings = mobile ? { ...settings, layout: settings.mobile_layout, shape: settings.mobile_shape, swatch_style: settings.mobile_swatch_style || settings.swatch_style, grid_max_columns: settings.mobile_grid_max_columns, swatch_gap: settings.mobile_swatch_gap, } : settings; const buttonCustomSizing = toBoolean(settings.button_custom_sizing, false); const numberCustomSizing = toBoolean(settings.number_custom_sizing, false); return { tooltipsEnabled: toBoolean(settings.enable_tooltips, true), // Tooltip animation + fade lifecycle. tooltipAnimation: ONE_OF( settings.tooltip_animation, TOOLTIP_ANIMATIONS, "fade", ), tooltipFadeIn: toBoolean(settings.tooltip_fade_in, true), tooltipFadeOut: toBoolean(settings.tooltip_fade_out, true), // Tooltip position, colors, typography and the mobile cutoff. tooltipPosition: ONE_OF( settings.tooltip_position, TOOLTIP_POSITIONS, "top", ), tooltipBgColor: hexColor(settings.tooltip_bg_color, "#1f2937"), tooltipTextColor: hexColor(settings.tooltip_text_color, "#ffffff"), tooltipFontSize: clampInt(settings.tooltip_font_size, 8, 20, 12), tooltipFontFamily: typeof settings.tooltip_font_family === "string" && settings.tooltip_font_family.trim() !== "" ? settings.tooltip_font_family.trim() : "inherit", disableTooltipsOnMobile: toBoolean( settings.disable_tooltips_on_mobile, false, ), mobileCutoff: clampInt(settings.mobile_cutoff, 320, 1200, 768), // Shape + selected-style. shape: ONE_OF(layoutSettings.shape, SHAPES, "square"), swatchStyle: ONE_OF( layoutSettings.swatch_style, SWATCH_STYLES, "underline", ), outlineBorderWidth: clampInt(settings.outline_border_width, 1, 6, 2), // Layout. All five layouts (wrap/grid/flow/pill/slider) are base // layouts; ONE_OF coerces only unknown values back to "wrap". // Slider renders via SliderLayout (track + ResizeObserver). layout: ONE_OF(layoutSettings.layout, LAYOUTS, "wrap"), gridMaxColumns: clampInt(layoutSettings.grid_max_columns, 1, 10, 4), flowJustify: ONE_OF(settings.flow_justify, FLOW_JUSTIFY, "center"), flowAlign: ONE_OF(settings.flow_align, FLOW_ALIGN, "center"), flowRowGap: clampInt(settings.flow_row_gap, 0, 100, 16), flowColumnGap: clampInt(settings.flow_column_gap, 0, 100, 16), layoutPaddingTop: clampPadding(settings.layout_padding_top, 5), layoutPaddingRight: clampPadding(settings.layout_padding_right, 5), layoutPaddingBottom: clampPadding(settings.layout_padding_bottom, 5), layoutPaddingLeft: clampPadding(settings.layout_padding_left, 5), externalPaddingTop: clampPadding(settings.external_padding_top, 5), externalPaddingRight: clampPadding(settings.external_padding_right, 5), externalPaddingBottom: clampPadding(settings.external_padding_bottom, 5), externalPaddingLeft: clampPadding(settings.external_padding_left, 5), paddingTop: clampPadding(settings.padding_top, 8), paddingRight: clampPadding(settings.padding_right, 16), paddingBottom: clampPadding(settings.padding_bottom, 8), paddingLeft: clampPadding(settings.padding_left, 16), showLabels: toBoolean(settings.show_labels, true), // Match the PHP default when older localized payloads omit this field so // cached pages do not briefly reveal color labels. showLabelsColors: toBoolean(settings.show_labels_colors, false), selectedColor: typeof settings.selected_color === "string" && settings.selected_color.trim() ? settings.selected_color : "#46a5e5", swatchWidth, swatchHeight, swatchGap: clampInt(layoutSettings.swatch_gap, 0, 40, 8), labelFontSize: clampInt(settings.label_font_size, 10, 24, 14), buttonSwatchWidth: buttonCustomSizing ? clampInt(settings.button_swatch_width, 24, 60, swatchWidth) : swatchWidth, buttonSwatchHeight: buttonCustomSizing ? clampInt(settings.button_swatch_height, 24, 60, swatchHeight) : swatchHeight, // Image swatch settings (free extension types). imageFit: ONE_OF(settings.image_fit, IMAGE_FITS, "cover"), showLabelsImages: toBoolean(settings.show_labels_images, false), // Number swatch settings (free extension types). numberCustomSizing, numberSwatchWidth: numberCustomSizing ? clampInt(settings.number_swatch_width, 24, 60, swatchWidth) : swatchWidth, numberSwatchHeight: numberCustomSizing ? clampInt(settings.number_swatch_height, 24, 60, swatchHeight) : swatchHeight, numberTextColorOverride: typeof settings.number_text_color_override === "string" ? settings.number_text_color_override.trim() : "", numberFontFamily: typeof settings.number_font_family === "string" && settings.number_font_family.trim() ? settings.number_font_family : "inherit", numberFontWeight: settings.number_font_weight !== undefined && settings.number_font_weight !== null && String(settings.number_font_weight).trim() ? String(settings.number_font_weight) : "600", // Dual-color swatch settings (free extension types). dualColorEnabled: toBoolean(settings.dual_color_enabled, true), dualColorCut: ONE_OF( settings.dual_color_cut, DUAL_COLOR_CUTS, "diagonal", ), dualColorGradient: ONE_OF( settings.dual_color_gradient, DUAL_COLOR_GRADIENTS, "none", ), // Single-product out-of-stock handling (free contract). outOfStockBehavior: ONE_OF( settings.out_of_stock_behavior, OUT_OF_STOCK_BEHAVIORS, "cross", ), outOfStockDisable: toBoolean(settings.out_of_stock_disable, false), }; }, [settings, viewportWidth]); }