import type { Swatch, SwatchRulesData } from '../contexts/ov25-ui-context.js'; import type { BedAllowNonePartsInput } from '../lib/config/bed-embed-query.js'; import type { StringReplacementsConfig } from './string-replacements.js'; export type { StringReplacementRuleTrigger, StringReplacementDefinitionKey, StringReplacementRule, StringReplacementsConfig, StringReplacementDefinition, StringInterpolationValueDefinition, StringReplacements, } from './string-replacements.js'; export { optionNameReplacement } from './string-replacements.js'; export type StringOrFunction = string | (() => string); export type ElementConfig = { /** CSS selector (id, class, or any valid selector). */ selector?: string; /** @deprecated Use `selector` instead. Supported for backward compatibility. */ id?: string; replace?: boolean; }; export type ElementSelector = string | ElementConfig; /** Responsive value: desktop and mobile breakpoints. */ export type ResponsiveValue = { desktop: T; mobile?: T; }; /** Carousel display: 'none' hides it, 'carousel' | 'stacked' show it. */ export type CarouselDisplayMode = 'none' | 'carousel' | 'stacked'; export type CarouselConfig = ResponsiveValue & { /** Max images to show in carousel; excess are cut. Responsive: { desktop, mobile }. */ maxImages?: number | ResponsiveValue; }; export type ConfiguratorDisplayMode = 'inline' | 'sheet' | 'drawer' | 'modal' | 'inline-sheet'; export type ConfiguratorConfig = { displayMode: ResponsiveValue; triggerStyle?: ResponsiveValue<'single-button' | 'split-buttons'>; variants?: VariantsConfig; modules?: ModulesConfig; }; /** Variant UI style: wizard | list | tabs | accordion | tree. Accordion not available on mobile. */ export type VariantDisplayMode = 'wizard' | 'list' | 'tabs' | 'accordion' | 'tree'; /** Snap2 / inline-sheet: which horizontal edge the variant settings sheet attaches to. */ export type Snap2VariantSheetSide = 'LEFT' | 'RIGHT'; /** * Snap2 (sheet / drawer / modal): where the compatible-modules UI is shown when not using {@link ConfiguratorDisplayMode} `inline` or `inline-sheet` (those modes always keep modules in the variants column; this value is ignored there). * If this matches {@link Snap2VariantSheetSide} (both LEFT or both RIGHT), modules render inside the variant sheet; otherwise a separate module rail/sheet is used. `BOTTOM` uses the bottom dock. */ export type Snap2ModulePanelPosition = 'LEFT' | 'RIGHT' | 'BOTTOM'; export type ModulesConfig = { position?: ResponsiveValue; }; export type VariantsConfig = { displayMode: ResponsiveValue; position?: ResponsiveValue; useSimpleVariantsSelector?: boolean; /** * Option ids or display names (case-insensitive) to omit from variant UI (list, wizard, tabs, etc.). * Iframe defaults and CURRENT_SKU state stay applied; users cannot change these options in the shell. */ hideOptions?: string[]; }; export type SelectorsConfig = { /** Root target for full-page display modes that own their own shell. */ root?: ElementSelector; gallery?: ElementSelector; price?: ElementSelector; name?: ElementSelector; variants?: ElementSelector; swatches?: ElementSelector; configureButton?: ElementSelector; initialiseMenu?: ElementSelector; }; /** * Maps option names to their selected SKU values. * Keys: option names (e.g. 'Color', 'Size'); reserved keys: 'Range', 'Product', 'Ranges', 'Products'. * @example { Color: 'RED-001', Size: 'M-001', Range: 'RANGE-123' } */ export interface OptionSkuMap { [optionName: string]: string; } /** One billable/configured product line after normalization (SKU side). */ export interface CommerceLineItemSku { /** Catalogue product id or other stable line id from the iframe. */ id: string; skuString: string; skuMap: Record; quantity: number; } /** One configured option row inside a price line (single-product breakdown or Snap2 selections). */ export interface CommerceLineItemSelection { category?: string; name: string; sku?: string; price: number; formattedPrice: string; thumbnail?: string; } /** One billable product line after normalization (price side). */ export interface CommerceLineItemPrice { /** Same id semantics as {@link CommerceLineItemSku.id} when both messages come from Snap2 with `productId`. */ id: string; name: string; quantity: number; price: number; formattedPrice: string; subtotal: number; formattedSubtotal: string; discountedAmount: number; formattedDiscountAmount: string; discountPercentage: number; selections: CommerceLineItemSelection[]; /** Snap2 3D model id when present on the iframe breakdown. */ modelId?: string; } /** One selection row inside {@link ProductPriceBreakdown} (Snap2; aligns with OV25 `useSnap2TotalPrice`). */ export interface SelectionPriceBreakdown { name: string; price: number; formattedPrice: string; thumbnail?: string; } /** * Per-product Snap2 totals (aligns with OV25 `ProductPriceBreakdown` in `useSnap2TotalPrice`). * Optional on {@link UnifiedPricePayload} for host consumers (cart, invoices). */ export interface ProductPriceBreakdown { productId: string; formattedSubtotal: string; subtotal: number; discountedAmount: number; formattedDiscountAmount: string; discountPercentage: number; name: string; quantity: number; price: number; formattedPrice: string; selections: SelectionPriceBreakdown[]; modelId: string; image?: string; } /** Single-product iframe: legacy top-level sku fields plus `lines` (length 1). */ export interface UnifiedSkuPayloadSingle { mode: 'single'; lines: CommerceLineItemSku[]; skuString: string; skuMap?: OptionSkuMap; } /** Multi-product (Snap2): only `lines`; no top-level `skuString`. */ export interface UnifiedSkuPayloadMulti { mode: 'multi'; lines: CommerceLineItemSku[]; } export type UnifiedSkuPayload = UnifiedSkuPayloadSingle | UnifiedSkuPayloadMulti; export interface UnifiedPricePayload { mode: 'single' | 'multi'; totalPrice: number; subtotal: number; formattedPrice: string; formattedSubtotal: string; discount: { amount: number; formattedAmount: string; percentage: number; }; lines: CommerceLineItemPrice[]; /** Raw single-product `priceBreakdown` from the iframe when present. */ priceBreakdown?: unknown[]; /** Snap2 `productBreakdowns` from the iframe when present (same shape as OV25 `ProductPriceBreakdown[]`). */ productBreakdowns?: ProductPriceBreakdown[]; } /** * Callback payload: normalized SKU and price. Each half is null until that message was received at least once. */ export interface UnifiedOnChangePayload { skus: UnifiedSkuPayload | null; price: UnifiedPricePayload | null; } export type OnChangePayload = UnifiedOnChangePayload; /** * Normalized SKU from CURRENT_SKU. Use `mode` / `lines` for multi-product; in `single` mode top-level `skuString` remains for legacy code. * @deprecated Prefer the name {@link UnifiedSkuPayload} in new integrations. */ export type OnChangeSkuPayload = UnifiedSkuPayload; /** * Normalized price from CURRENT_PRICE (includes `mode`, `lines`, optional raw breakdown passthrough). * @deprecated Prefer the name {@link UnifiedPricePayload} in new integrations. */ export type OnChangePricePayload = UnifiedPricePayload; /** Callbacks for inject configurator. */ export interface CallbacksConfig { /** Add configured product to basket/checkout. Receives current { skus, price } so you don't need to store them in state. */ addToBasket: (payload?: OnChangePayload) => void; /** Buy now / checkout immediately. Receives current { skus, price } so you don't need to store them in state. */ buyNow: (payload?: OnChangePayload) => void; /** Purchase selected swatches */ buySwatches: (swatches: Swatch[], swatchRulesData: SwatchRulesData) => void; /** * Called when price or SKU changes from configurator messages. * Receives normalized `{ skus, price }` (see {@link UnifiedOnChangePayload}); each is `null` until that message type has been received at least once. */ onChange?: (payload: OnChangePayload) => void; } export type BrandingConfig = { logoURL?: string; mobileLogoURL?: string; cssString?: string; hideLogo?: boolean; }; export type FlagsConfig = { hidePricing?: boolean; disableAddToCart?: boolean; hideAr?: boolean; deferThreeD?: boolean; showOptional?: boolean; /** Force mobile layout for testing (e.g. in device frame). */ forceMobile?: boolean; /** Auto-open configurator modal on load. Only applies when not using inline display mode. Default false. */ autoOpen?: boolean; /** * Display symbol for formatted prices from the iframe (OV25 emits GBP/`£`). Replaces `£` in `CURRENT_PRICE` * strings after normalization; not FX conversion. Default `£`. */ currencySymbol?: string; }; /** Per bed line: when true, variant UI hides selections whose `metadata.bedSize` ≠ iframe current size. */ export type BedPartSizeFilterFlags = { headboard: boolean; base: boolean; mattress: boolean; }; /** Bed iframe (OV25): optional `bedAllowNone` query param on the configurator URL. */ export type BedEmbedConfig = { allowNone?: BedAllowNonePartsInput; filterSelectionsByCurrentSize?: BedPartSizeFilterFlags; }; export type DiningDisplayOptions = { /** Show in-scene dining attachment point buttons. Default true. */ showAttachmentPoints?: boolean; }; export type DiningStyleImagesConfig = { /** Image for the fixed/full-range style choice. */ fullRange?: string; /** Image for the mix-and-match style choice. */ mixAndMatch?: string; }; export type DiningEmbedConfig = { displayMode?: ResponsiveValue<'split' | 'full-page'>; displayOptions?: DiningDisplayOptions; /** Optional hero images for the initial full-page style-choice screen. */ styleImages?: DiningStyleImagesConfig; }; /** Primary inject config: grouped structure. */ export interface InjectConfiguratorOptions { apiKey: StringOrFunction; productLink: StringOrFunction; configurationUuid?: StringOrFunction; images?: string[]; uniqueId?: string; selectors: SelectorsConfig; carousel?: CarouselConfig; configurator?: ConfiguratorConfig; /** Callbacks for add to basket, buy now, buy swatches, and onChange (price/SKU updates) */ callbacks: CallbacksConfig; branding?: BrandingConfig; flags?: FlagsConfig; bed?: BedEmbedConfig; stringReplacements?: StringReplacementsConfig; dining?: DiningEmbedConfig; } /** Legacy flat config. Supported for backward compatibility. */ export interface LegacyInjectConfiguratorOptions { apiKey: StringOrFunction; productLink: StringOrFunction; configurationUuid?: StringOrFunction; images?: string[]; uniqueId?: string; stringReplacements?: StringReplacementsConfig; gallerySelector?: ElementSelector; rootSelector?: ElementSelector; rootId?: ElementSelector; priceSelector?: ElementSelector; nameSelector?: ElementSelector; variantsSelector?: ElementSelector; swatchesSelector?: ElementSelector; configureButtonSelector?: ElementSelector; initialiseMenuSelector?: ElementSelector; carouselDisplayMode?: CarouselDisplayMode; carouselDisplayModeMobile?: CarouselDisplayMode; configuratorDisplayMode?: 'inline' | 'sheet' | 'modal' | 'inline-sheet'; configuratorDisplayModeMobile?: 'inline' | 'drawer' | 'modal'; configuratorTriggerStyle?: 'single-button' | 'split-buttons'; configuratorTriggerStyleMobile?: 'single-button' | 'split-buttons'; variantDisplayMode?: VariantDisplayMode; variantDisplayModeMobile?: VariantDisplayMode; useSimpleVariantsSelector?: boolean; addToBasketFunction: (payload?: OnChangePayload) => void; buyNowFunction: (payload?: OnChangePayload) => void; buySwatchesFunction: (swatches: Swatch[], swatchRulesData: SwatchRulesData) => void; onChangeFunction?: (payload: OnChangePayload) => void; logoURL?: string; mobileLogoURL?: string; cssString?: string; hideLogo?: boolean; hidePricing?: boolean; disableAddToCart?: boolean; hideAr?: boolean; deferThreeD?: boolean; showOptional?: boolean; forceMobile?: boolean; autoOpen?: boolean; /** @see {@link FlagsConfig.currencySymbol} */ currencySymbol?: string; /** @see {@link BedEmbedConfig} */ bedAllowNone?: BedAllowNonePartsInput; /** @see {@link BedEmbedConfig.filterSelectionsByCurrentSize} */ bedFilterSelectionsByCurrentSize?: BedPartSizeFilterFlags; /** @see {@link DiningDisplayOptions.showAttachmentPoints} */ showDiningAttachmentPoints?: boolean; /** @see {@link DiningEmbedConfig.displayMode} */ diningDisplayMode?: 'split' | 'full-page'; /** @see {@link DiningEmbedConfig.displayMode} */ diningDisplayModeMobile?: 'split' | 'full-page'; /** @see {@link DiningEmbedConfig.styleImages} */ diningFullRangeImageURL?: string; /** @see {@link DiningEmbedConfig.styleImages} */ diningMixAndMatchImageURL?: string; galleryId?: ElementSelector; priceId?: ElementSelector; nameId?: ElementSelector; variantsId?: ElementSelector; swatchesId?: ElementSelector; configureButtonId?: ElementSelector; initialiseMenuId?: ElementSelector; variantDisplayStyle?: VariantDisplayMode; variantDisplayStyleMobile?: VariantDisplayMode; useInlineVariantControls?: boolean; hideOptions?: string[]; } export type InjectConfiguratorInput = InjectConfiguratorOptions | LegacyInjectConfiguratorOptions; /** Internal flattened config used by inject logic. */ export interface NormalizedInjectConfig { apiKey: StringOrFunction; productLink: StringOrFunction; configurationUuid?: StringOrFunction; images?: string[]; uniqueId?: string; stringReplacements?: StringReplacementsConfig; gallerySelector?: ElementSelector; rootSelector?: ElementSelector; priceSelector?: ElementSelector; nameSelector?: ElementSelector; variantsSelector?: ElementSelector; swatchesSelector?: ElementSelector; configureButtonSelector?: ElementSelector; initialiseMenuSelector?: ElementSelector; carouselDisplayMode: CarouselDisplayMode; carouselDisplayModeMobile: CarouselDisplayMode; carouselMaxImagesDesktop?: number; carouselMaxImagesMobile?: number; configuratorDisplayMode: 'inline' | 'sheet' | 'modal' | 'inline-sheet'; configuratorDisplayModeMobile: 'inline' | 'drawer' | 'modal'; configuratorTriggerStyle: 'single-button' | 'split-buttons'; configuratorTriggerStyleMobile: 'single-button' | 'split-buttons'; variantDisplayMode: VariantDisplayMode; variantDisplayModeMobile: VariantDisplayMode; useSimpleVariantsSelector: boolean; /** Lowercase trimmed option ids/names to hide from variant selectors (see {@link VariantsConfig.hideOptions}). */ hideVariantOptions: string[]; addToBasketFunction: (payload?: OnChangePayload) => void; buyNowFunction: (payload?: OnChangePayload) => void; buySwatchesFunction: (swatches: Swatch[], swatchRulesData: SwatchRulesData) => void; onChangeFunction?: (payload: OnChangePayload) => void; logoURL?: string; mobileLogoURL?: string; cssString?: string; hideLogo: boolean; hidePricing?: boolean; disableAddToCart?: boolean; hideAr?: boolean; deferThreeD?: boolean; showOptional?: boolean; forceMobile?: boolean; autoOpen?: boolean; /** Serialized `bedAllowNone` query value; omit when unset (OV25 default: all parts may use None). */ bedAllowNoneQueryValue?: string; /** When false, adds `showAttachmentPoints=false` to dining iframe URLs. */ diningShowAttachmentPoints?: boolean; /** Dining UI shell display mode. */ diningDisplayModeDesktop: 'split' | 'full-page'; diningDisplayModeMobile: 'split' | 'full-page'; /** Dining full-page style choice hero images. */ diningStyleImages?: DiningStyleImagesConfig; /** Bed variant UI: hide non-matching `metadata.bedSize` per line when enabled. */ bedFilterSelectionsByCurrentSize: BedPartSizeFilterFlags; /** Display symbol for iframe price strings; see {@link FlagsConfig.currencySymbol}. */ currencySymbol: string; /** Snap2 variant rail (normalized lowercase for UI). Defaults: right (desktop/mobile). */ snap2VariantSheetSideDesktop: 'left' | 'right'; snap2VariantSheetSideMobile: 'left' | 'right'; /** Snap2 module picker placement. Default bottom (desktop/mobile). */ snap2ModulePanelPositionDesktop: 'left' | 'right' | 'bottom'; snap2ModulePanelPositionMobile: 'left' | 'right' | 'bottom'; } export declare function normalizeInjectConfig(opts: InjectConfiguratorInput): NormalizedInjectConfig;