From b4da89cdc9a521323c33672ab8adba83c43854dd Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Wed, 29 Apr 2026 12:19:19 -0300 Subject: [PATCH 1/2] Show Harbor version in the feature manager admin page --- resources/js/components/templates/AppShell.tsx | 11 ++++++++++- resources/js/components/templates/Shell.tsx | 2 +- resources/js/types/harbor-data.ts | 1 + src/Harbor/Admin/Feature_Manager_Page.php | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/resources/js/components/templates/AppShell.tsx b/resources/js/components/templates/AppShell.tsx index 04378063..389eed2f 100644 --- a/resources/js/components/templates/AppShell.tsx +++ b/resources/js/components/templates/AppShell.tsx @@ -6,7 +6,7 @@ * * @package LiquidWeb\Harbor */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Shell } from '@/components/templates/Shell'; import { FilterBar } from '@/components/molecules/FilterBar'; import { LicensePanel } from '@/components/organisms/LicensePanel'; @@ -58,6 +58,15 @@ export function AppShell() { ) ) } + + { window.harborData?.version && ( +
+

+ { /* translators: %s: plugin version number */ } + { sprintf( __( 'Version %s', '%TEXTDOMAIN%' ), window.harborData.version ) } +

+
+ ) } ); diff --git a/resources/js/components/templates/Shell.tsx b/resources/js/components/templates/Shell.tsx index 895962a5..c328a14b 100644 --- a/resources/js/components/templates/Shell.tsx +++ b/resources/js/components/templates/Shell.tsx @@ -23,7 +23,7 @@ export function Shell( { header, sideContent, children }: ShellProps ) { { header }
-
+
{ children }
\n );\n}\n","/**\n * Info banner shown when all licensed products are unactivated on this domain.\n *\n * Fires when every product's validation_status is 'not_activated' or\n * 'activation_required'. Links to the Liquid Web portal so the user can\n * activate their license for this domain.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { __ } from '@wordpress/i18n';\nimport { useSelect } from '@wordpress/data';\nimport { Info } from 'lucide-react';\nimport { store as harborStore } from '@/store';\n\n/**\n * @since 1.0.0\n */\nexport function NotActivatedBanner() {\n\tconst allNotActivated = useSelect(\n\t\t( select ) => select( harborStore ).areAllProductsNotActivated(),\n\t\t[]\n\t);\n\tconst licenseKey = useSelect(\n\t\t( select ) => select( harborStore ).getLicenseKey(),\n\t\t[]\n\t);\n\n\tif ( ! allNotActivated || ! licenseKey || ! window.harborData ) return null;\n\n\tconst activationUrl = window.harborData.activationUrl;\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t

\n\t\t\t\t{ __(\n\t\t\t\t\t'None of your products are activated for this domain. Activate your license to unlock features.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t) }\n\t\t\t\t{ ' ' }\n\t\t\t\t\n\t\t\t\t\t{ __( 'Activate now', '%TEXTDOMAIN%' ) }\n\t\t\t\t\n\t\t\t

\n\t\t\n\t);\n}\n","/**\n * Persistent banner shown when feature toggles require a page reload.\n *\n * Uses role=\"status\" + aria-live=\"polite\" so screen readers announce it once\n * when it appears, without interrupting the current focus.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { __ } from '@wordpress/i18n';\nimport { RefreshCw } from 'lucide-react';\nimport { useReloadBanner } from '@/context/reload-banner-context';\n\n/**\n * @since 1.0.0\n */\nexport function ReloadBanner() {\n const { needsReload } = useReloadBanner();\n\n return (\n
\n { needsReload && (\n window.location.reload() }\n className=\"flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-1.5 text-xs text-amber-800 hover:bg-amber-100 transition-colors\"\n >\n \n { __( 'Reload page to see changes', '%TEXTDOMAIN%' ) }\n \n ) }\n
\n );\n}\n","/**\n * Collapsible accordion grouping locked features under a tier header.\n *\n * Shows the tier name, feature count, a lock indicator, and an upgrade\n * button. Expanding the accordion reveals the locked FeatureRow entries.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { useState } from 'react';\nimport { __ } from '@wordpress/i18n';\nimport { ChevronRight, ChevronDown, Lock, ExternalLink } from 'lucide-react';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { LicenseBadge } from '@/components/atoms/LicenseBadge';\nimport { FeatureRow } from '@/components/molecules/FeatureRow';\nimport type { CatalogTier, Feature } from '@/types/api';\n\ninterface TierGroupProps {\n tier: CatalogTier;\n features: Feature[];\n forceOpen?: boolean;\n showUpgrade?: boolean;\n /**\n * When true, renders an Unactivated badge in place of the upgrade button.\n * Used when the user owns this tier but has not yet activated the license\n * on the current domain.\n */\n showUnactivated?: boolean;\n /**\n * Target URL for the upgrade button. Resolved by the parent so the\n * component doesn't need to know whether the user has an existing\n * subscription (change-plan URL) or is purchasing fresh (purchase_url).\n */\n buttonHref?: string;\n}\n\n/**\n * @since TBD Added showUnactivated prop to render an Unactivated badge in place of the upgrade button.\n * @since 1.0.0\n */\nexport function TierGroup( { tier, features, forceOpen = false, showUpgrade = true, showUnactivated = false, buttonHref }: TierGroupProps ) {\n const [ expanded, setExpanded ] = useState( false );\n const isOpen = expanded || forceOpen;\n const Chevron = isOpen ? ChevronDown : ChevronRight;\n\n return (\n <>\n
\n setExpanded( ! expanded ) }\n className=\"flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity\"\n >\n \n \n { tier.name } { __( 'Features', '%TEXTDOMAIN%' ) }\n \n \n { features.length }\n \n \n
\n { showUpgrade && buttonHref && (\n window.open( buttonHref, '_blank', 'noopener,noreferrer' ) }\n >\n \n { __( 'Upgrade to', '%TEXTDOMAIN%' ) }{ ' ' }{ tier.name }\n \n ) }\n { showUnactivated && (\n \n ) }\n \n\n { isOpen && features.map( ( feature ) => (\n \n ) ) }\n \n );\n}\n","/**\n * Upsell card for a product not covered by the current license.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { __ } from '@wordpress/i18n';\nimport { ExternalLink } from 'lucide-react';\nimport { ProductLogo } from '@/components/atoms/ProductLogo';\nimport type { Product } from '@/types/api';\n\nconst UPSELL_TAGLINES: Record = {\n\tgive: __( 'Beautiful donation forms & fundraising', '%TEXTDOMAIN%' ),\n\t'the-events-calendar': __( 'Tickets, RSVPs & event management', '%TEXTDOMAIN%' ),\n\tlearndash: __( 'Sell courses & manage learners', '%TEXTDOMAIN%' ),\n\tkadence: __( 'Themes, blocks & design tools', '%TEXTDOMAIN%' ),\n};\n\ninterface UpsellCardProps {\n\tproduct: Product;\n\thref: string;\n}\n\n/**\n * @since 1.0.0\n */\nexport function UpsellCard( { product, href }: UpsellCardProps ) {\n\treturn (\n\t\t\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{ product.name }\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t{ UPSELL_TAGLINES[ product.slug ] ?? product.tagline }\n\t\t\t\t\n\t\t\t
\n\t\t\t\n\t\t\n\t);\n}\n","/**\n * Displays the installed/available version of a feature, with an update\n * button when a newer version is available.\n *\n * When upgradeLabel is provided the update button is rendered fully disabled\n * (no onClick handler) with an upsell tooltip.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { sprintf, __ } from '@wordpress/i18n';\nimport { UpdateButton } from '@/components/atoms/UpdateButton';\nimport type { Feature } from '@/types/api';\n\nexport interface VersionDisplayProps {\n\tfeature: Feature;\n\t/** When set, the update button is disabled and this text is shown as an upsell tooltip. */\n\tupgradeLabel?: string;\n\tpendingAction?: 'enabling' | 'disabling' | 'installing' | 'updating' | null;\n\tinstallableBusy?: boolean;\n\t/** Required when upgradeLabel is not set and the button should be active. */\n\tonUpdate?: () => void;\n}\n\n/**\n * @since 1.0.0\n */\nexport function VersionDisplay( {\n\tfeature,\n\tupgradeLabel,\n\tpendingAction = null,\n\tinstallableBusy = false,\n\tonUpdate,\n}: VersionDisplayProps ) {\n\tif ( feature.update_version ) {\n\t\treturn (\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\tv{ feature.installed_version }\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tv{ feature.update_version }\n\t\t\t\t\n\t\t\t\t{ ( upgradeLabel || onUpdate ) && (\n\t\t\t\t\t\n\t\t\t\t) }\n\t\t\t
\n\t\t);\n\t}\n\n\tif ( ! feature.version && ! feature.installed_version ) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t\n\t\t\t{ `v${ feature.installed_version ?? feature.version }` }\n\t\t\n\t);\n}\n","/**\n * Error modal organism.\n *\n * Renders when the ErrorModalContext holds active errors. Lists each error\n * and provides a Dismiss button so the user can close the modal and interact\n * with the UI (e.g. to update the license key).\n *\n * @package LiquidWeb\\Harbor\n */\nimport { __ } from '@wordpress/i18n';\nimport { ExternalLink, Mail } from 'lucide-react';\nimport { useErrorModal } from '@/context/error-modal-context';\nimport { Dialog, DialogContent, DialogFooter, DialogHeader } from '@/components/ui/dialog';\nimport { ErrorItem } from '@/components/atoms/ErrorItem';\nimport { Button } from '@/components/ui/button';\n\nconst DOCS_URL = 'https://go.liquidweb.com/harbor-docs';\nconst SUPPORT_URL = 'https://go.liquidweb.com/harbor-support';\n\n/**\n * @since 1.0.0\n */\nexport function ErrorModal() {\n const { errors, clearAll } = useErrorModal();\n\n if ( errors.length === 0 ) return null;\n\n return (\n \n \n \n
    \n { errors.map( ( error ) => (\n \n ) ) }\n
\n
\n \n
\n

\n { __( 'Need help resolving this?', '%TEXTDOMAIN%' ) }\n

\n
\n \n { __( 'View documentation', '%TEXTDOMAIN%' ) }\n \n \n \n { __( 'Contact support', '%TEXTDOMAIN%' ) }\n \n \n
\n
\n \n
\n
\n );\n}\n","/**\n * License sidebar panel.\n *\n * Always visible. Fetches license and catalog data from the store and passes\n * it to LicenseSection and UpsellSection.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { useMemo } from 'react';\nimport { __ } from '@wordpress/i18n';\nimport { useSelect, useDispatch } from '@wordpress/data';\nimport { LicenseSection } from '@/components/organisms/LicenseSection';\nimport { UpsellSection } from '@/components/organisms/UpsellSection';\nimport { store as harborStore } from '@/store';\nimport { PRODUCTS } from '@/data/products';\nimport { useToast } from '@/context/toast-context';\nimport { useErrorModal } from '@/context/error-modal-context';\nimport { HarborError } from '@/errors';\n\n/**\n * @since 1.0.0\n */\nexport function LicensePanel() {\n const { addToast } = useToast();\n const { addError } = useErrorModal();\n const { deleteLicense, refreshLicense, refreshCatalog } = useDispatch( harborStore );\n\n const { licenseKey, licenseProducts, catalogs, isRefreshing, isLicenseLoading } = useSelect(\n ( select ) => ({\n licenseKey: select( harborStore ).getLicenseKey(),\n licenseProducts: select( harborStore ).getLicenseProducts(),\n catalogs: select( harborStore ).getCatalog(),\n isRefreshing: select( harborStore ).isLicenseRefreshing(),\n // @ts-expect-error -- hasFinishedResolution is injected at runtime by @wordpress/data but absent from the store's TypeScript surface.\n isLicenseLoading: ! select( harborStore ).hasFinishedResolution( 'getLicenseKey', [] ),\n }),\n []\n );\n\n // Flat tier slug → display name and rank lookups from all catalog tiers.\n const { tierNameMap, tierRankMap } = useMemo( () => {\n const names: Record = {};\n const ranks: Record = {};\n catalogs.forEach( ( catalog ) => {\n catalog.tiers.forEach( ( t ) => {\n names[ t.tier_slug ] = t.name;\n ranks[ t.tier_slug ] = t.rank;\n } );\n } );\n return { tierNameMap: names, tierRankMap: ranks };\n }, [ catalogs ] );\n\n const activationUrl = licenseKey && window.harborData ? window.harborData.activationUrl : null;\n\n // Product slug → lowest paid-tier purchase URL map from the catalog.\n const upsellUrlMap = useMemo( () => {\n const map: Record = {};\n catalogs.forEach( ( catalog ) => {\n const sorted = catalog.tiers.slice().sort( ( a, b ) => a.rank - b.rank );\n const paidTier = sorted.find( ( t ) => t.rank > 0 );\n if ( paidTier?.purchase_url ) {\n map[ catalog.product_slug ] = paidTier.purchase_url;\n }\n } );\n return map;\n }, [ catalogs ] );\n\n const licensedSlugs = new Set( licenseProducts.map( ( lp ) => lp.product_slug ) );\n const upsellProducts = PRODUCTS.filter( ( p ) => ! licensedSlugs.has( p.slug ) );\n\n const handleRemove = async (): Promise => {\n const result = await deleteLicense();\n if ( result instanceof HarborError ) {\n addError( result );\n return result;\n }\n addToast( __( 'License removed.', '%TEXTDOMAIN%' ), 'default' );\n return null;\n };\n\n const handleRefresh = async () => {\n const [ licenseResult, catalogResult ] = await Promise.all( [\n refreshLicense(),\n refreshCatalog(),\n ] );\n if ( licenseResult instanceof HarborError ) {\n addError( licenseResult );\n }\n if ( catalogResult instanceof HarborError ) {\n addError( catalogResult );\n }\n if ( ! ( licenseResult instanceof HarborError ) && ! ( catalogResult instanceof HarborError ) ) {\n addToast( __( 'License refreshed.', '%TEXTDOMAIN%' ), 'success' );\n }\n };\n\n return (\n
\n \n { ! isLicenseLoading && (\n \n ) }\n
\n );\n}\n","/**\n * License section: header, key input, and licensed-product cards.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { useMemo, useState } from 'react';\nimport { __ } from '@wordpress/i18n';\nimport { KeyRound, Loader2, RefreshCw } from 'lucide-react';\nimport { SectionHeader } from '@/components/atoms/SectionHeader';\nimport { LicenseKeyInputSkeleton } from '@/components/atoms/LicenseKeyInputSkeleton';\nimport { LicenseKeyInput } from '@/components/molecules/LicenseKeyInput';\nimport { LicenseProductCard } from '@/components/molecules/LicenseProductCard';\nimport { PRODUCTS } from '@/data/products';\nimport { groupLicenseProducts } from '@/lib/group-license-products';\nimport type { LicenseProduct } from '@/types/api';\nimport type HarborError from '@/errors/harbor-error';\n\ninterface LicenseSectionProps {\n licenseKey: string | null;\n licenseProducts: LicenseProduct[];\n tierNameMap: Record;\n tierRankMap: Record;\n onRemove: () => Promise;\n onRefresh: () => Promise;\n isRefreshing: boolean;\n isLoading: boolean;\n activationUrl: string | null;\n}\n\n/**\n * Pulse-skeleton that mirrors LicenseProductCard's layout while the license\n * resolver is still in flight.\n */\nfunction LicenseSectionSkeleton() {\n return (\n
\n { PRODUCTS.map( ( p ) => (\n
\n
\n { /* logo */ }\n
\n { /* product name */ }\n
\n { /* tier badge */ }\n
\n { /* chevron */ }\n
\n
\n
\n ) ) }\n
\n );\n}\n\n/**\n * @since 1.0.0\n */\nexport function LicenseSection( {\n licenseKey,\n licenseProducts,\n tierNameMap,\n tierRankMap,\n onRemove,\n onRefresh,\n isRefreshing,\n isLoading,\n activationUrl,\n}: LicenseSectionProps ) {\n const [ isEditing, setIsEditing ] = useState( false );\n\n const hasLicense = licenseKey !== null;\n const manageUrl = window.harborData?.subscriptionsUrl ?? null;\n\n const handleRemove = async (): Promise => {\n const error = await onRemove();\n if ( ! error ) {\n setIsEditing( false );\n }\n return error;\n };\n\n const groupedProducts = useMemo(\n () => groupLicenseProducts( licenseProducts, tierRankMap ),\n [ licenseProducts, tierRankMap ],\n );\n\n return (\n
\n }\n label={ __( 'License', '%TEXTDOMAIN%' ) }\n action={ (\n \n { isRefreshing\n ? \n : \n }\n { isRefreshing\n ? __( 'Refreshing...', '%TEXTDOMAIN%' )\n : __( 'Refresh', '%TEXTDOMAIN%' )\n }\n \n ) }\n />\n\n { isLoading ? (\n <>\n \n \n \n ) : (\n <>\n setIsEditing( true ) }\n onCancel={ () => setIsEditing( false ) }\n onRemove={ handleRemove }\n onSuccess={ () => setIsEditing( false ) }\n />\n { ! hasLicense && (\n

\n { __( 'Enter your license key to unlock features.', '%TEXTDOMAIN%' ) }\n

\n ) }\n \n ) }\n\n { ! isLoading && hasLicense && groupedProducts.length > 0 && (\n
\n { groupedProducts.map( ( g ) => (\n t.is_valid && t.activated_here ) }` }\n productSlug={ g.productSlug }\n productName={ g.productName }\n tiers={ g.tiers }\n tierNameMap={ tierNameMap }\n activationUrl={ activationUrl }\n />\n ) ) }\n\n { manageUrl && (\n

\n \n { __( 'Manage license in Liquid Web', '%TEXTDOMAIN%' ) }\n \n

\n ) }\n
\n ) }\n
\n );\n}\n","/**\n * Product section: sticky dark header + feature list + tier group accordions.\n *\n * Available features render as FeatureRow entries. Locked features are\n * grouped by tier and rendered inside collapsible TierGroup accordions.\n *\n * Header counts (active / deactivated) always reflect the full unfiltered\n * feature set so they remain stable while the user searches.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { __ } from '@wordpress/i18n';\nimport { useSelect } from '@wordpress/data';\nimport { LicenseBadge } from '@/components/atoms/LicenseBadge';\nimport { ProductLogo } from '@/components/atoms/ProductLogo';\nimport { FeatureRow } from '@/components/molecules/FeatureRow';\nimport { TierGroup } from '@/components/molecules/TierGroup';\nimport { store as harborStore } from '@/store';\nimport { useFilter } from '@/context/filter-context';\nimport { useProductFeatureGroups } from '@/hooks/useProductFeatureGroups';\nimport { buildChangePlanUrl } from '@/lib/change-plan-url';\nimport type { Product } from '@/types/api';\n\ninterface ProductSectionProps {\n product: Product;\n}\n\n/**\n * @since TBD Show Unactivated badge on tier groups and product header for unactivated licenses; route upgrade button to change-plan URL for existing subscribers.\n * @since 1.0.0\n */\nexport function ProductSection( { product }: ProductSectionProps ) {\n const { searchQuery } = useFilter();\n const isSearching = searchQuery.trim().length > 0;\n\n // Full unfiltered set — used only for header counts so they stay stable.\n const { licenseProduct, hasActiveLegacy, unactivatedLicenseProduct } = useSelect(\n ( select ) => {\n const licenseProducts = select( harborStore ).getLicenseProducts();\n const forProduct = licenseProducts.filter( ( lp ) => lp.product_slug === product.slug );\n return {\n licenseProduct: forProduct.find( ( lp ) => lp.activated_here === true ) ?? null,\n hasActiveLegacy: select( harborStore ).hasActiveLegacyLicenseForProduct( product.slug ),\n unactivatedLicenseProduct: select( harborStore ).getUnactivatedLicenseProduct( product.slug ),\n };\n },\n [ product.slug ],\n );\n\n const { availableFeatures, lockedByTier, sortedCatalogTiers, upgradeCatalogTiers, activationCatalogTiers, isUnactivatedLicense } = useProductFeatureGroups( product.slug );\n\n const activeCount = availableFeatures.filter( ( f ) => f.is_enabled ).length;\n const deactivatedCount = availableFeatures.filter( ( f ) => ! f.is_enabled ).length;\n\n // Show \"Unactivated\" in the header only when there is no activated product at all.\n // An unactivated upgrade tier alongside an active lower tier (e.g. pro active + elite\n // unactivated) should still show the active tier's name — not \"Unactivated\".\n const isNotActivated = ( licenseProduct === null && isUnactivatedLicense ) || (\n licenseProduct !== null && (\n licenseProduct.validation_status === 'not_activated' ||\n licenseProduct.validation_status === 'activation_required'\n )\n );\n\n const tierName = licenseProduct\n ? ( sortedCatalogTiers.find( ( t ) => t.tier_slug === licenseProduct.tier )?.name ?? licenseProduct.tier )\n : null;\n\n const hasContent = availableFeatures.length > 0 ||\n Object.values( lockedByTier ).some( ( f ) => f.length > 0 );\n\n return (\n
\n\t\t\t
\n
\n \n

\n { product.name }\n

\n { isNotActivated ? (\n \n ) : tierName ? (\n \n ) : hasActiveLegacy ? (\n \n ) : (\n \n ) }\n \n { activeCount } { __( 'active', '%TEXTDOMAIN%' ) }\n { ' · ' }\n { deactivatedCount } { __( 'deactivated', '%TEXTDOMAIN%' ) }\n \n
\n\n { isSearching && ! hasContent && (\n
\n

\n { __( 'No features match your search.', '%TEXTDOMAIN%' ) }\n

\n
\n ) }\n\n { ! isSearching && ! hasContent && (\n
\n

\n { __( 'No features are available for this product.', '%TEXTDOMAIN%' ) }\n

\n
\n ) }\n\n { hasContent && (\n
\n { availableFeatures.map( ( feature ) => (\n \n ) ) }\n\n { activationCatalogTiers.map( ( tier ) => {\n const locked = lockedByTier[ tier.tier_slug ] ?? [];\n if ( locked.length === 0 ) return null;\n return (\n \n );\n } ) }\n\n { upgradeCatalogTiers.map( ( tier ) => {\n const locked = lockedByTier[ tier.tier_slug ] ?? [];\n if ( locked.length === 0 ) return null;\n\n // Any user with an existing subscription — activated or not — is\n // routed to the portal's change-plan flow so the upgrade modifies\n // their existing subscription. Truly unlicensed visitors fall back\n // to the catalog's purchase_url so they can buy fresh.\n const subscriptionsUrl = window.harborData?.subscriptionsUrl;\n const effectiveLicenseProduct = licenseProduct ?? unactivatedLicenseProduct;\n const buttonHref = effectiveLicenseProduct && subscriptionsUrl\n ? buildChangePlanUrl( subscriptionsUrl, product.slug, tier.tier_slug )\n : tier.purchase_url;\n\n return (\n \n );\n } ) }\n
\n ) }\n
\n );\n}\n","/**\n * Pulse-skeleton for a single product section, shown while the Harbor data\n * resolvers are in flight on the first page load.\n *\n * Mirrors ProductSection's DOM structure: same sticky header (with real logo\n * and product name but no badge or counters) followed by a fixed number of\n * skeleton feature rows.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { ProductLogo } from '@/components/atoms/ProductLogo';\nimport type { Product } from '@/types/api';\n\nconst SKELETON_ROW_COUNT = 3;\n\nfunction SkeletonFeatureRow( { isLast }: { isLast: boolean } ) {\n return (\n
\n
\n { /* chevron */ }\n
\n { /* feature icon */ }\n
\n { /* feature name */ }\n
\n { /* right: status badge + switch */ }\n
\n
\n
\n
\n
\n
\n );\n}\n\ninterface ProductSectionSkeletonProps {\n product: Product;\n}\n\n/**\n * @since 1.0.0\n */\nexport function ProductSectionSkeleton( { product }: ProductSectionSkeletonProps ) {\n return (\n
\n
\n
\n \n

\n { product.name }\n

\n
\n
\n { Array.from( { length: SKELETON_ROW_COUNT }, ( _, i ) => (\n \n ) ) }\n
\n
\n );\n}\n","/**\n * Upsell section: products not covered by the current license.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { __ } from '@wordpress/i18n';\nimport { Rocket } from 'lucide-react';\nimport { SectionHeader } from '@/components/atoms/SectionHeader';\nimport { UpsellCard } from '@/components/molecules/UpsellCard';\nimport type { Product } from '@/types/api';\n\ninterface UpsellSectionProps {\n products: Product[];\n upsellUrlMap: Record;\n}\n\n/**\n * @since 1.0.0\n */\nexport function UpsellSection( { products, upsellUrlMap }: UpsellSectionProps ) {\n if ( products.length === 0 ) return null;\n\n return (\n <>\n
\n\n
\n }\n label={ __( 'Add to your plan', '%TEXTDOMAIN%' ) }\n />\n
\n { products.map( ( p ) => (\n \n ) ) }\n
\n
\n \n );\n}\n","/**\n * Application shell — full-width two-column layout.\n *\n * Main area: FilterBar header + product sections.\n * Sidebar: license panel.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { __ } from '@wordpress/i18n';\nimport { Shell } from '@/components/templates/Shell';\nimport { FilterBar } from '@/components/molecules/FilterBar';\nimport { LicensePanel } from '@/components/organisms/LicensePanel';\nimport { LegacyLicenseBanner } from '@/components/molecules/LegacyLicenseBanner';\nimport { NotActivatedBanner } from '@/components/molecules/NotActivatedBanner';\nimport { ReloadBanner } from '@/components/molecules/ReloadBanner';\nimport { ProductSection } from '@/components/organisms/ProductSection';\nimport { ProductSectionSkeleton } from '@/components/organisms/ProductSectionSkeleton';\nimport { ErrorBoundary } from '@/components/atoms/ErrorBoundary';\nimport { PRODUCTS } from '@/data/products';\nimport { useFilter } from '@/context/filter-context';\nimport { useHarborData } from '@/context/harbor-data-context';\n\n/**\n * @since 1.0.0\n */\nexport function AppShell() {\n const { isLoading } = useHarborData();\n\n const { productFilter } = useFilter();\n\n const visibleProducts = productFilter === 'all'\n ? PRODUCTS\n : PRODUCTS.filter( ( p ) => p.slug === productFilter );\n\n return (\n }\n sideContent={ }\n >\n \n
\n \n \n\n
\n

{ __( 'Your Features', '%TEXTDOMAIN%' ) }

\n
\n\n { isLoading\n ? PRODUCTS.map( ( product ) => (\n \n ) )\n : visibleProducts.map( ( product ) => (\n \n ) )\n }\n
\n
\n \n );\n}\n","/**\n * Two-column page shell: scrollable main area + sticky sidebar.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { ReactNode } from 'react';\n\ninterface ShellProps {\n /** Optional content rendered above children (FilterBar slot). */\n header?: ReactNode;\n /** Content rendered in the right sidebar. */\n sideContent?: ReactNode;\n children: ReactNode;\n}\n\n/**\n * @since 1.0.0\n */\nexport function Shell( { header, sideContent, children }: ShellProps ) {\n return (\n
\n
\n { header }\n
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{ children }\n\t\t\t\t
\n\t\t\t\t\n\t\t\t
\n
\n );\n}\n","import * as React from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '@/lib/utils';\n\nconst badgeVariants = cva(\n 'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',\n {\n variants: {\n variant: {\n\t\t\t\tdefault:\n\t\t\t\t\t\"bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n\t\t\t\tsecondary:\n\t\t\t\t\t\"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n\t\t\t\tdestructive:\n\t\t\t\t\t\"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n\t\t\t\toutline:\n\t\t\t\t\t\"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n\t\t\t\tghost:\n\t\t\t\t\t\"[a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n\t\t\t\tlink:\n\t\t\t\t\t\"text-primary underline-offset-4 [a&]:hover:underline\",\n\t\t\t\tsuccess:\n\t\t\t\t\t\"bg-emerald-100 text-emerald-800 border-emerald-200\",\n\t\t\t\tgradient:\n\t\t\t\t\t\"bg-gradient-to-r from-emerald-500 to-emerald-600 text-white border-0\",\n\t\t\t\twarning:\n\t\t\t\t\t\"bg-amber-100 text-amber-800 border-amber-200\",\n\t\t\t\tinfo:\n\t\t\t\t\t\"bg-blue-100 text-blue-800 border-blue-200\",\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n }\n);\n\nexport interface BadgeProps\n extends React.HTMLAttributes,\n VariantProps {}\n\nfunction Badge( { className, variant, ...props }: BadgeProps ) {\n return (\n \n );\n}\n\nexport { Badge, badgeVariants };\n","import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n \"cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps & {\n asChild?: boolean\n }) {\n const Comp = asChild ? Slot.Root : \"button\"\n\n return (\n \n )\n}\n\nexport { Button, buttonVariants }\n","/**\n * Custom modal dialog.\n *\n * Uses z-[100000] on the overlay so it clears the WP admin bar (z-index: 99999).\n * NOT using Radix Dialog — keeping this self-contained to control z-index.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { useEffect, type ReactNode } from 'react';\nimport { createPortal } from 'react-dom';\nimport { X } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { __ } from '@wordpress/i18n';\n\ninterface DialogProps {\n open: boolean;\n onClose: () => void;\n children: ReactNode;\n /** Max width class, defaults to \"max-w-lg\" */\n maxWidth?: string;\n}\n\n/**\n * Dialog overlay + panel. Traps focus via the backdrop click handler.\n * @since 1.0.0\n */\nexport function Dialog( { open, onClose, children, maxWidth = 'max-w-lg' }: DialogProps ) {\n // Close on Escape key\n useEffect( () => {\n if ( ! open ) return;\n const handleKey = ( e: KeyboardEvent ) => {\n if ( e.key === 'Escape' ) onClose();\n };\n document.addEventListener( 'keydown', handleKey );\n return () => document.removeEventListener( 'keydown', handleKey );\n }, [ open, onClose ] );\n\n // Prevent body scroll when open\n useEffect( () => {\n if ( open ) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = '';\n }\n return () => {\n document.body.style.overflow = '';\n };\n }, [ open ] );\n\n if ( ! open ) return null;\n\n const portalTarget = document.getElementById( 'lw-harbor-root' ) ?? document.body;\n\n return createPortal(\n \n {/* Backdrop */}\n \n\n {/* Panel */}\n e.stopPropagation() }\n >\n { children }\n
\n
,\n portalTarget\n );\n}\n\ninterface DialogHeaderProps {\n title: string;\n description?: string;\n onClose: () => void;\n}\n\nexport function DialogHeader( { title, description, onClose }: DialogHeaderProps ) {\n return (\n
\n
\n

{ title }

\n { description && (\n

{ description }

\n ) }\n
\n \n \n \n
\n );\n}\n\nexport function DialogContent( { children, className }: { children: ReactNode; className?: string } ) {\n return (\n
\n { children }\n
\n );\n}\n\nexport function DialogFooter( { children, className }: { children: ReactNode; className?: string } ) {\n return (\n
\n { children }\n
\n );\n}\n","import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n return (\n \n )\n}\n\nexport { Input }\n","/**\n * Radix UI Select primitives styled to the project's design system.\n *\n * Intentionally NOT using Select.Portal — portal content renders outside\n * .lw-harbor-ui and would be invisible to the PostCSS scope plugin (all Tailwind\n * utilities are scoped to .lw-harbor-ui). The Content renders in the DOM tree but\n * Radix positions it with position:fixed so it still floats above other elements.\n *\n * @package LiquidWeb\\Harbor\n */\nimport * as React from 'react';\nimport { Select as SelectPrimitive } from 'radix-ui';\nimport { ChevronDown, ChevronUp, Check } from 'lucide-react';\nimport { cn } from '@/lib/utils';\n\nconst Select = SelectPrimitive.Root;\nconst SelectGroup = SelectPrimitive.Group;\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>( ( { className, children, ...props }, ref ) => (\n span]:line-clamp-1',\n 'outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n className\n ) }\n { ...props }\n >\n { children }\n \n \n \n \n) );\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>( ( { className, ...props }, ref ) => (\n \n \n \n) );\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>( ( { className, ...props }, ref ) => (\n \n \n \n) );\nSelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>( ( { className, children, position = 'popper', ...props }, ref ) => (\n \n \n \n { children }\n \n \n \n) );\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>( ( { className, ...props }, ref ) => (\n \n) );\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>( ( { className, children, ...props }, ref ) => (\n \n \n \n \n \n \n { children }\n \n) );\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>( ( { className, ...props }, ref ) => (\n \n) );\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n Select,\n SelectGroup,\n SelectValue,\n SelectTrigger,\n SelectContent,\n SelectLabel,\n SelectItem,\n SelectSeparator,\n SelectScrollUpButton,\n SelectScrollDownButton,\n};\n","\"use client\"\n\nimport * as React from \"react\"\nimport { Switch as SwitchPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Switch({\n className,\n size = \"default\",\n ...props\n}: React.ComponentProps & {\n size?: \"sm\" | \"default\"\n}) {\n return (\n \n \n \n )\n}\n\nexport { Switch }\n","/**\n * Toast notification renderer.\n *\n * Reads from useToastStore and renders a fixed bottom-right stack.\n * Auto-dismiss is handled by the store (3.5s).\n *\n * @package LiquidWeb\\Harbor\n */\nimport { X, CheckCircle, AlertTriangle, Info } from 'lucide-react';\nimport { __ } from '@wordpress/i18n';\nimport { cn } from '@/lib/utils';\nimport { useToast, type ToastVariant } from '@/context/toast-context';\n\nconst VARIANT_STYLES: Record = {\n default: 'bg-background border border-border text-foreground',\n success: 'bg-green-50 border border-green-200 text-green-800',\n error: 'bg-red-50 border border-red-200 text-red-800',\n warning: 'bg-amber-50 border border-amber-200 text-amber-800',\n};\n\nfunction ToastIcon( { variant }: { variant: ToastVariant } ) {\n if ( variant === 'success' ) return ;\n if ( variant === 'error' ) return ;\n if ( variant === 'warning' ) return ;\n return ;\n}\n\n/**\n * Renders the toast stack. Mount as a sibling of AppShell in App.tsx.\n * @since 1.0.0\n */\nexport function Toaster() {\n const { toasts, removeToast } = useToast();\n\n return (\n \n { toasts.map( ( toast ) => (\n \n \n
\n { toast.message }\n { toast.action && (\n { toast.action!.onClick(); removeToast( toast.id ); } }\n className=\"self-start text-xs font-medium underline underline-offset-2 hover:no-underline\"\n >\n { toast.action.label }\n \n ) }\n
\n removeToast( toast.id ) }\n className=\"shrink-0 opacity-60 hover:opacity-100 transition-opacity\"\n aria-label={ __( 'Dismiss notification', '%TEXTDOMAIN%' ) }\n >\n \n \n
\n ) ) }\n
\n );\n}\n","/**\n * Single-component tooltip wrapper built on Radix UI.\n *\n * Uses a Portal + inline styles instead of Tailwind utilities because the\n * Portal teleports content outside .lw-harbor-ui, where the PostCSS scope plugin\n * would no longer apply. z-index 100001 clears the WP admin bar (99999) and\n * our dialogs (100000).\n *\n * The children are wrapped in a so that hover detection still works\n * when the child element has pointer-events disabled (e.g. a disabled button).\n *\n * Usage:\n * \n * \n * \n *\n * @package LiquidWeb\\Harbor\n */\nimport { Tooltip as TooltipPrimitive } from 'radix-ui';\n\ninterface TooltipProps {\n\tlabel: string;\n\tchildren: React.ReactNode;\n\tclassName?: string;\n\tstyle?: React.CSSProperties;\n}\n\n/**\n * @since 1.0.0\n */\nexport function Tooltip( { label, children, className, style }: TooltipProps ) {\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{ children }\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{ label }\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t);\n}\n","/**\n * Error modal context — surfaces resolver and API errors in a dismissible\n * modal while keeping the full UI rendered.\n *\n * Mount once in App.tsx; consume with useErrorModal()\n * anywhere in the component tree.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { createContext, useCallback, useContext, useState, type ReactNode } from 'react';\nimport type HarborError from '@/errors/harbor-error';\n\ninterface ErrorModalContextValue {\n errors: HarborError[];\n addError: ( error: HarborError ) => void;\n removeError: ( code: string ) => void;\n clearAll: () => void;\n}\n\nconst ErrorModalContext = createContext( {\n errors: [],\n addError: () => {},\n removeError: () => {},\n clearAll: () => {},\n} );\n\n/**\n * @since 1.0.0\n */\nexport function ErrorModalProvider( { children }: { children: ReactNode } ) {\n const [ errors, setErrors ] = useState( [] );\n\n const addError = useCallback( ( error: HarborError ) => {\n setErrors( ( prev ) =>\n prev.some( ( e ) => e.code === error.code ) ? prev : [ ...prev, error ]\n );\n }, [] );\n\n const removeError = useCallback( ( code: string ) => {\n setErrors( ( prev ) => prev.filter( ( e ) => e.code !== code ) );\n }, [] );\n\n const clearAll = useCallback( () => setErrors( [] ), [] );\n\n return (\n \n { children }\n \n );\n}\n\n/**\n * @since 1.0.0\n */\nexport const useErrorModal = () => useContext( ErrorModalContext );\n","/**\n * Filter context — shared search query and product filter state.\n *\n * Mount once in App.tsx; consume with useFilter() anywhere\n * in the component tree.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { createContext, useContext, useState, type ReactNode } from 'react';\n\ninterface FilterContextValue {\n searchQuery: string;\n productFilter: string;\n setSearchQuery: ( q: string ) => void;\n setProductFilter: ( slug: string ) => void;\n}\n\nconst FilterContext = createContext( {\n searchQuery: '',\n productFilter: 'all',\n setSearchQuery: () => {},\n setProductFilter: () => {},\n} );\n\n/**\n * @since 1.0.0\n */\nexport function FilterProvider( { children }: { children: ReactNode } ) {\n const [ searchQuery, setSearchQuery ] = useState( '' );\n const [ productFilter, setProductFilter ] = useState( 'all' );\n\n return (\n \n { children }\n \n );\n}\n\n/**\n * @since 1.0.0\n */\nexport const useFilter = () => useContext( FilterContext );\n","/**\n * Harbor admin screen data context.\n *\n * Owns the four core resolvers for the Harbor admin screen (license, features,\n * catalog, legacy licenses). Errors from any resolver are pushed to the\n * ErrorModalContext so the error modal opens while the full UI stays rendered.\n * Errors are automatically cleared when all resolvers recover.\n *\n * Mount inside and outside\n * so it remains alive through render crashes.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { createContext, useContext, useEffect, useRef, type ReactNode } from 'react';\nimport { __ } from '@wordpress/i18n';\nimport { useSelect } from '@wordpress/data';\nimport { store as harborStore } from '@/store';\nimport useResolvableSelect from '@/hooks/use-resolvable-select/use-resolvable-select';\nimport HarborError from '@/errors/harbor-error';\nimport { ErrorCode } from '@/errors/error-code';\nimport { useErrorModal } from '@/context/error-modal-context';\nimport type { ResolvableSelectResponse } from '@/hooks/use-resolvable-select/types';\n\ninterface HarborDataContextValue {\n isLoading: boolean;\n}\n\nconst HarborDataContext = createContext( {\n isLoading: true,\n} );\n\ntype ResolvableRecord = Record>;\n\nconst RESOLVER_KEYS = [ 'license', 'features', 'catalog', 'legacyLicenses' ] as const;\ntype ResolverKey = typeof RESOLVER_KEYS[ number ];\n\nfunction findErrors( results: ResolvableRecord ): HarborError[] {\n const errors: HarborError[] = [];\n for ( const key in results ) {\n const entry = results[ key ];\n if ( entry.status === 'ERROR' ) {\n errors.push( HarborError.syncFrom(\n entry.error,\n ErrorCode.ResolutionFailed,\n __( 'Liquid Web Software Manager failed to load your data.', '%TEXTDOMAIN%' ),\n ) );\n }\n }\n return errors;\n}\n\n/**\n * @since 1.0.0\n */\nexport function HarborDataProvider( { children }: { children: ReactNode } ) {\n const { addError, removeError } = useErrorModal();\n const lastErrorCodesRef = useRef( [] );\n\n const result = useResolvableSelect(\n ( resolve ) => ( {\n license: resolve( harborStore ).getLicenseKey(),\n features: resolve( harborStore ).getFeatures(),\n catalog: resolve( harborStore ).getCatalog(),\n legacyLicenses: resolve( harborStore ).getLegacyLicenses(),\n } ),\n [],\n );\n\n const hasEverResolvedRef = useRef>( {\n license: false,\n features: false,\n catalog: false,\n legacyLicenses: false,\n } );\n\n for ( const key of RESOLVER_KEYS ) {\n if ( result[ key ].hasResolved ) {\n\t\t\thasEverResolvedRef.current[ key ] = true;\n\t\t}\n }\n\n const isLoading = RESOLVER_KEYS.some( ( key ) => result[ key ].isResolving && ! hasEverResolvedRef.current[ key ] );\n\n useEffect( () => {\n const found = findErrors( result );\n\n if ( found.length > 0 ) {\n lastErrorCodesRef.current = found.map( ( e ) => e.code );\n found.forEach( ( error ) => addError( error ) );\n } else if ( lastErrorCodesRef.current.length > 0 ) {\n lastErrorCodesRef.current.forEach( ( code ) => removeError( code ) );\n lastErrorCodesRef.current = [];\n }\n }, [ result, addError, removeError ] );\n\n const licenseError = useSelect(\n ( select ) => select( harborStore ).getLicenseError(),\n []\n );\n\n const lastLicenseErrorCodeRef = useRef( null );\n\n useEffect( () => {\n if ( licenseError !== null ) {\n const error = new HarborError( ErrorCode.LicenseValidateFailed, licenseError.message );\n lastLicenseErrorCodeRef.current = error.code;\n addError( error );\n } else if ( lastLicenseErrorCodeRef.current !== null ) {\n removeError( lastLicenseErrorCodeRef.current );\n lastLicenseErrorCodeRef.current = null;\n }\n }, [ licenseError, addError, removeError ] );\n\n return (\n \n { children }\n \n );\n}\n\n/**\n * @since 1.0.0\n */\nexport const useHarborData = () => useContext( HarborDataContext );\n","/**\n * @package LiquidWeb\\Harbor\n */\nimport { createContext, useContext, useState, type ReactNode } from 'react';\n\ninterface ReloadBannerContextValue {\n needsReload: boolean;\n setNeedsReload: ( value: boolean ) => void;\n}\n\nconst ReloadBannerContext = createContext( {\n needsReload: false,\n setNeedsReload: () => {},\n} );\n\n/**\n * @since 1.0.0\n */\nexport function ReloadBannerProvider( { children }: { children: ReactNode } ) {\n const [ needsReload, setNeedsReload ] = useState( false );\n\n return (\n \n { children }\n \n );\n}\n\n/**\n * @since 1.0.0\n */\nexport const useReloadBanner = () => useContext( ReloadBannerContext );\n","/**\n * Toast notification context — replaces Zustand toast-store.ts.\n *\n * Mount once in App.tsx; consume with useToast() anywhere\n * in the component tree.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { createContext, useCallback, useContext, useRef, useState, type ReactNode } from 'react';\nimport { __ } from '@wordpress/i18n';\n\nexport type ToastVariant = 'default' | 'success' | 'error' | 'warning';\n\nexport interface ToastAction {\n label: string;\n onClick: () => void;\n}\n\nexport interface Toast {\n id: string;\n message: string;\n variant: ToastVariant;\n action?: ToastAction;\n}\n\ninterface ToastContextValue {\n toasts: Toast[];\n addToast: ( message: string, variant?: ToastVariant, action?: ToastAction ) => void;\n removeToast: ( id: string ) => void;\n}\n\nconst ToastContext = createContext( {\n toasts: [],\n addToast: () => {},\n removeToast: () => {},\n} );\n\n/**\n * @since 1.0.0\n */\nexport function ToastProvider( { children }: { children: ReactNode } ) {\n const [ toasts, setToasts ] = useState( [] );\n const counterRef = useRef( 0 );\n\n const removeToast = useCallback( ( id: string ) => {\n setToasts( ( prev ) => prev.filter( ( t ) => t.id !== id ) );\n }, [] );\n\n const addToast = useCallback(\n ( message: string, variant: ToastVariant = 'default', action?: ToastAction ) => {\n const id = `lw-harbor-toast-id-${ ++counterRef.current }`;\n\n setToasts( ( prev ) => [ ...prev, { id, message, variant, action } ] );\n if ( ! action ) {\n setTimeout( () => removeToast( id ), 3500 );\n }\n },\n [ removeToast ],\n );\n\n return (\n \n { children }\n \n );\n}\n\n/**\n * @since 1.0.0\n */\nexport const useToast = () => useContext( ToastContext );\n","/**\n * Product catalog data.\n *\n * Product metadata. Tier definitions and feature lists come from the\n * liquidweb/harbor/v1/catalog and liquidweb/harbor/v1/features REST\n * endpoints — not stored here.\n *\n * @package LiquidWeb\\Harbor\n */\nimport type { Product } from '@/types/api';\n\nexport const PRODUCTS: Product[] = [\n {\n slug: 'give',\n name: 'GiveWP',\n tagline: 'Donation forms and fundraising for WordPress',\n },\n {\n slug: 'the-events-calendar',\n name: 'The Events Calendar',\n tagline: 'Powerful event management for WordPress',\n },\n {\n slug: 'learndash',\n name: 'LearnDash',\n tagline: 'World-class LMS for online courses',\n },\n {\n slug: 'kadence',\n name: 'Kadence',\n tagline: 'Page builder and theme toolkit for WordPress',\n },\n];\n\n/** Lookup a product by slug */\nexport function getProduct( slug: string ): Product | undefined {\n return PRODUCTS.find( ( p ) => p.slug === slug );\n}\n","/**\n * Machine-readable error codes for HarborError instances.\n *\n * @package LiquidWeb\\Harbor\n */\nexport enum ErrorCode {\n\tFeaturesFetchFailed = 'features-fetch-failed',\n\tFeatureEnableFailed = 'feature-enable-failed',\n\tFeatureDisableFailed = 'feature-disable-failed',\n\tFeatureUpdateFailed = 'feature-update-failed',\n\tLicenseFetchFailed = 'license-fetch-failed',\n\tLicenseActionInProgress = 'license-action-in-progress',\n\tLicenseStoreFailed = 'license-store-failed',\n\tLicenseDeleteFailed = 'license-delete-failed',\n\tLicenseRefreshFailed = 'license-refresh-failed',\n\tLicenseValidateFailed = 'license-validate-failed',\n\tCatalogFetchFailed = 'catalog-fetch-failed',\n\tCatalogRefreshFailed = 'catalog-refresh-failed',\n\tLegacyLicensesFetchFailed = 'legacy-licenses-fetch-failed',\n\tResolutionFailed = 'resolution-failed',\n}\n","/**\n * HarborError -- typed wrapper around the WP REST API serialized WP_Error.\n *\n * @wordpress/api-fetch throws the parsed JSON body (a plain object) when\n * the server returns a non-2xx response. HarborError normalizes that into\n * a proper Error subclass with structured access to code, data, and any\n * additional errors.\n *\n * The entire error chain is typed. `additionalErrors` contains HarborError\n * instances (not plain WpRestError objects), so consumers get `.code`,\n * `.status`, and `.data` on every entry without casting.\n *\n * @package LiquidWeb\\Harbor\n */\n\nimport type { WpRestError } from './types';\nimport { isWpRestError } from './utils';\nimport { ErrorCode } from './error-code';\n\nexport default class HarborError extends Error {\n\t/**\n\t * Machine-readable error code from the WP_Error.\n\t */\n\treadonly code: string;\n\n\t/**\n\t * Data payload (usually contains `{ status: number }`).\n\t */\n\treadonly data: Record;\n\n\t/**\n\t * Secondary errors from a multi-code WP_Error response. This is a\n\t * deserialization concern only. Use `cause` (via `HarborError.wrap()`)\n\t * to chain errors on the frontend.\n\t */\n\treadonly additionalErrors: HarborError[];\n\n\t/**\n\t * Original cause, if this error wraps another.\n\t */\n\treadonly cause?: Error;\n\n\tconstructor(code: ErrorCode, message: string, options?: { cause?: Error });\n\tconstructor(wpError: WpRestError, options?: { cause?: Error });\n\tconstructor(\n\t\tcodeOrError: ErrorCode | WpRestError,\n\t\tmessageOrOptions?: string | { cause?: Error },\n\t\toptions?: { cause?: Error }\n\t) {\n\t\tif (typeof codeOrError === 'string') {\n\t\t\tsuper(messageOrOptions as string);\n\t\t\tthis.name = 'HarborError';\n\t\t\tthis.code = codeOrError;\n\t\t\tthis.data = {};\n\t\t\tthis.additionalErrors = [];\n\t\t\tthis.cause = options?.cause;\n\t\t} else {\n\t\t\tsuper(codeOrError.message);\n\t\t\tthis.name = 'HarborError';\n\t\t\tthis.code = codeOrError.code;\n\t\t\tthis.data = codeOrError.data ?? {};\n\t\t\tthis.additionalErrors = (codeOrError.additional_errors ?? []).map(\n\t\t\t\t(entry) => new HarborError(entry)\n\t\t\t);\n\t\t\tthis.cause = (messageOrOptions as { cause?: Error } | undefined)?.cause;\n\t\t}\n\t}\n\n\t/**\n\t * HTTP status code, if present.\n\t */\n\tget status(): number | undefined {\n\t\treturn typeof this.data.status === 'number'\n\t\t\t? this.data.status\n\t\t\t: undefined;\n\t}\n\n\t/**\n\t * Flatten the error tree into an array. Collects this error, then its\n\t * additionalErrors (server-side siblings), then recurses into cause.\n\t */\n\ttoArray(): HarborError[] {\n\t\tconst result: HarborError[] = [this];\n\t\tfor (const additional of this.additionalErrors) {\n\t\t\tresult.push(...additional.toArray());\n\t\t}\n\t\tif (this.cause instanceof HarborError) {\n\t\t\tresult.push(...this.cause.toArray());\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Async conversion of an unknown value into an HarborError.\n\t *\n\t * Handles everything `syncFrom` does, plus `Response` objects that\n\t * apiFetch throws when it cannot parse JSON or when `parse: false`\n\t * is used.\n\t */\n\tstatic async from(\n\t\terror: unknown,\n\t\tcode: ErrorCode,\n\t\tmessage: string\n\t): Promise {\n\t\tif (error instanceof Response) {\n\t\t\ttry {\n\t\t\t\tconst body = await error.json();\n\t\t\t\tif (isWpRestError(body)) {\n\t\t\t\t\treturn new HarborError(body);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Response body wasn't JSON, fall through.\n\t\t\t}\n\n\t\t\treturn new HarborError(code, message);\n\t\t}\n\n\t\treturn HarborError.syncFrom(error, code, message);\n\t}\n\n\t/**\n\t * Synchronous conversion of an unknown value into an HarborError.\n\t *\n\t * If the value is already an HarborError, returns it as-is. If it is\n\t * a WpRestError, hydrates it via the constructor. Anything else\n\t * (plain Error, string, etc.) produces an HarborError with the given\n\t * fallback `code` and `message`, and the original is stored as `cause`.\n\t */\n\tstatic syncFrom(\n\t\terror: unknown,\n\t\tcode: ErrorCode,\n\t\tmessage: string\n\t): HarborError {\n\t\tif (error instanceof HarborError) {\n\t\t\treturn error;\n\t\t}\n\n\t\tif (isWpRestError(error)) {\n\t\t\treturn new HarborError(error);\n\t\t}\n\n\t\tif (error instanceof Error) {\n\t\t\treturn new HarborError({ code, message }, { cause: error });\n\t\t}\n\n\t\treturn new HarborError({ code, message });\n\t}\n\n\t/**\n\t * Async wrap of an unknown caught value into an HarborError with context.\n\t *\n\t * The provided `code` and `message` describe what operation failed.\n\t * The original value is preserved as `cause` so the full error chain\n\t * is available for inspection. When the original is a WpRestError,\n\t * its `data` and `additional_errors` are also carried forward.\n\t *\n\t * Handles `Response` objects that apiFetch throws when it cannot\n\t * parse JSON or when `parse: false` is used.\n\t */\n\tstatic async wrap(\n\t\terror: unknown,\n\t\tcode: ErrorCode,\n\t\tmessage: string\n\t): Promise {\n\t\tif (error instanceof Response) {\n\t\t\ttry {\n\t\t\t\tconst body = await error.json();\n\t\t\t\tif (isWpRestError(body)) {\n\t\t\t\t\treturn new HarborError(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcode,\n\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\tdata: body.data,\n\t\t\t\t\t\t\tadditional_errors: body.additional_errors,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ cause: new HarborError(body) }\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Response body wasn't JSON, fall through.\n\t\t\t}\n\n\t\t\treturn new HarborError({ code, message });\n\t\t}\n\n\t\treturn HarborError.wrapSync(error, code, message);\n\t}\n\n\t/**\n\t * Synchronous wrap of an unknown caught value into an HarborError\n\t * with context.\n\t *\n\t * Same as `wrap` but cannot handle `Response` objects. Use this in\n\t * synchronous code paths where `await` is not available.\n\t */\n\tstatic wrapSync(error: unknown, code: ErrorCode, message: string): HarborError {\n\t\tif (error instanceof HarborError || error instanceof Error) {\n\t\t\treturn new HarborError({ code, message }, { cause: error });\n\t\t}\n\n\t\tif (isWpRestError(error)) {\n\t\t\treturn new HarborError(\n\t\t\t\t{\n\t\t\t\t\tcode,\n\t\t\t\t\tmessage,\n\t\t\t\t\tdata: error.data,\n\t\t\t\t\tadditional_errors: error.additional_errors,\n\t\t\t\t},\n\t\t\t\t{ cause: new HarborError(error) }\n\t\t\t);\n\t\t}\n\n\t\treturn new HarborError({ code, message });\n\t}\n}\n","export { default as HarborError } from './harbor-error';\nexport { ErrorCode } from './error-code';\nexport { isWpRestError } from './utils';\nexport type { WpRestError } from './types';\n","/**\n * Error utility functions.\n *\n * @package LiquidWeb\\Harbor\n */\nimport type { WpRestError } from './types';\n\n/**\n * Type guard. Checks whether an unknown value matches the WP REST error shape.\n */\nexport function isWpRestError( value: unknown ): value is WpRestError {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t'code' in value &&\n\t\ttypeof ( value as WpRestError ).code === 'string' &&\n\t\t'message' in value &&\n\t\ttypeof ( value as WpRestError ).message === 'string'\n\t);\n}\n","/**\n * Like useSelect, but selectors return objects containing\n * both the original data AND the resolution info.\n *\n * Ported from sync-saas and converted to TypeScript.\n *\n * Inspired by `@wordpress/core-data` `useQuerySelect`.\n *\n * @see https://github.com/WordPress/gutenberg/blob/c97c26fe371e3d40efe197d8f398326a16cdbf46/packages/core-data/src/hooks/use-query-select.ts\n *\n * @package LiquidWeb\\Harbor\n */\nimport type { DependencyList } from 'react';\nimport { useSelect } from '@wordpress/data';\nimport type {\n\tEnrichedSelectors,\n\tMapResolvableSelect,\n\tResolvableSelectResponse,\n\tStatus,\n} from './types';\n\n/**\n * Meta selectors added by @wordpress/data that should not be enriched.\n */\nconst META_SELECTORS = [\n\t'getIsResolving',\n\t'hasStartedResolution',\n\t'hasFinishedResolution',\n\t'isResolving',\n\t'getCachedResolvers',\n];\n\n/**\n * Cache enriched selector proxies by selector object identity so we\n * don't recreate them on every useSelect call within the same render.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst cache = new WeakMap>();\n\n/**\n * Wrap store selectors so each call returns a {@link ResolvableSelectResponse}\n * with the original data and resolution metadata.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction enrichSelectors( selectors: Record ): EnrichedSelectors {\n\tconst cached = cache.get( selectors );\n\tif ( cached ) {\n\t\treturn cached;\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst resolvers: Record = {};\n\n\tfor ( const selectorName in selectors ) {\n\t\tif ( META_SELECTORS.includes( selectorName ) ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tObject.defineProperty( resolvers, selectorName, {\n\t\t\tget:\n\t\t\t\t() =>\n\t\t\t\t( ...args: unknown[] ): ResolvableSelectResponse => {\n\t\t\t\t\tconst data = selectors[ selectorName ]( ...args );\n\t\t\t\t\tconst resolutionState = selectors.getResolutionState(\n\t\t\t\t\t\tselectorName,\n\t\t\t\t\t\targs,\n\t\t\t\t\t);\n\t\t\t\t\tconst resolutionStatus: string | undefined =\n\t\t\t\t\t\tresolutionState?.status;\n\n\t\t\t\t\tlet status: Status;\n\t\t\t\t\tswitch ( resolutionStatus ) {\n\t\t\t\t\t\tcase 'resolving':\n\t\t\t\t\t\t\tstatus = 'RESOLVING';\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'finished':\n\t\t\t\t\t\t\tstatus = 'SUCCESS';\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'error':\n\t\t\t\t\t\t\tstatus = 'ERROR';\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tstatus = 'IDLE';\n\t\t\t\t\t}\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata,\n\t\t\t\t\t\tstatus,\n\t\t\t\t\t\terror: resolutionState?.error ?? null,\n\t\t\t\t\t\tisResolving: status === 'RESOLVING',\n\t\t\t\t\t\thasStarted: status !== 'IDLE',\n\t\t\t\t\t\thasResolved:\n\t\t\t\t\t\t\tstatus === 'SUCCESS' || status === 'ERROR',\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t} );\n\t}\n\n\tcache.set( selectors, resolvers as EnrichedSelectors );\n\treturn resolvers as EnrichedSelectors;\n}\n\n/**\n * Like useSelect, but the selectors return objects containing\n * both the original data AND the resolution info.\n */\nexport default function useResolvableSelect(\n\tmapResolvableSelect: MapResolvableSelect,\n\tdeps: DependencyList,\n): T {\n\treturn useSelect(\n\t\t( select, registry ) => {\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\t\tconst resolve = ( store: any ) =>\n\t\t\t\tenrichSelectors( select( store ) );\n\t\t\treturn mapResolvableSelect( resolve, registry );\n\t\t},\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t\tdeps as unknown[],\n\t);\n}\n","/**\n * Behavior hook for FeatureRow.\n *\n * Encapsulates store wiring, async action handlers, and all derived state\n * so FeatureRow itself stays a pure composition of atoms.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { useState } from 'react';\nimport { __, sprintf } from '@wordpress/i18n';\nimport { useSelect, useDispatch } from '@wordpress/data';\nimport { store as harborStore } from '@/store';\nimport { getLicenseBadgeType } from '@/lib/feature-utils';\nimport type { LicenseBadgeType } from '@/lib/feature-utils';\nimport { useToast } from '@/context/toast-context';\nimport { useReloadBanner } from '@/context/reload-banner-context';\nimport { useErrorModal } from '@/context/error-modal-context';\nimport { HarborError } from '@/errors';\nimport type { Feature } from '@/types/api';\nimport { isInstallableFeature } from '@/types/utils';\nimport type { FeatureStatus } from '@/components/atoms/StatusBadge';\n\nexport type PendingAction = 'enabling' | 'disabling' | 'installing' | 'updating' | null;\n\nfunction getBadgeStatus(\n pendingAction: PendingAction,\n licenseBadgeType: LicenseBadgeType | null,\n featureEnabled: boolean\n): FeatureStatus {\n if ( pendingAction ) {\n return pendingAction as FeatureStatus;\n }\n if ( licenseBadgeType === 'revoked' && ! featureEnabled ) {\n return 'locked';\n }\n return featureEnabled ? 'enabled' : 'available';\n}\n\nfunction getSwitchChecked( pendingAction: PendingAction, featureEnabled: boolean ): boolean {\n if ( pendingAction === 'enabling' || pendingAction === 'installing' ) {\n return true;\n }\n if ( pendingAction === 'disabling' ) {\n return false;\n }\n return featureEnabled;\n}\n\nexport interface FeatureRowState {\n\tpendingAction: PendingAction;\n\tinstallableBusy: boolean;\n\tbadgeStatus: FeatureStatus;\n\tshowSwitch: boolean;\n\tswitchChecked: boolean;\n\tlicenseBadgeType: LicenseBadgeType | null;\n\tshowDeactivateConfirm: boolean;\n\thandleToggle: ( checked: boolean ) => Promise;\n\thandleUpdate: () => Promise;\n\thandleConfirmDeactivate: () => Promise;\n\thandleCancelDeactivate: () => void;\n}\n\n/**\n * @since 1.0.0\n */\nexport function useFeatureRow( feature: Feature ): FeatureRowState {\n\tconst { addToast } = useToast();\n\tconst { setNeedsReload } = useReloadBanner();\n\tconst { addError } = useErrorModal();\n\tconst { enableFeature, disableFeature, updateFeature } = useDispatch( harborStore );\n\n\tconst installableBusy = useSelect(\n\t\t( select ) =>\n\t\t\tisInstallableFeature( feature ) &&\n\t\t\tselect( harborStore ).isAnyInstallableBusy(),\n\t\t[ feature.type ]\n\t);\n\n\tconst enabledHarborHostCount = useSelect(\n\t\t( select ) => select( harborStore ).getEnabledHarborHostCount(),\n\t\t[]\n\t);\n\n\tconst harborHostBasenames = useSelect(\n\t\t( select ) => select( harborStore ).getHarborHostBasenames(),\n\t\t[]\n\t);\n\n\tconst isLegacy = useSelect(\n\t\t( select ) => {\n\t\t\tconst activeLegacy = select( harborStore ).getActiveLegacyLicense( feature.slug );\n\t\t\tif ( ! activeLegacy ) return false;\n\t\t\treturn ! select( harborStore ).isProductLicenseValid( feature.product );\n\t\t},\n\t\t[ feature.slug, feature.product ]\n\t);\n\n\tconst licenseBadgeType = getLicenseBadgeType( feature, isLegacy );\n\n\tconst [ pendingAction, setPendingAction ] = useState( null );\n\tconst [ showDeactivateConfirm, setShowDeactivateConfirm ] = useState( false );\n\n\t// Non-installable features (services) have no install/toggle/update lifecycle.\n\tif ( ! isInstallableFeature( feature ) ) {\n\t\treturn {\n\t\t\tpendingAction: null,\n\t\t\tinstallableBusy: false,\n\t\t\tbadgeStatus: 'included' as FeatureStatus,\n\t\t\tshowSwitch: false,\n\t\t\tswitchChecked: false,\n\t\t\tlicenseBadgeType,\n\t\t\tshowDeactivateConfirm: false,\n\t\t\thandleToggle: async () => {},\n\t\t\thandleUpdate: async () => {},\n\t\t\thandleConfirmDeactivate: async () => {},\n\t\t\thandleCancelDeactivate: () => {},\n\t\t};\n\t}\n\n\tconst featureEnabled = feature.is_enabled;\n\tconst featureInstalled = feature.installed_version !== null;\n\tconst isHarborHost = feature.type === 'plugin' && harborHostBasenames.includes( feature.plugin_file );\n\tconst isLastHarborHost = isHarborHost && featureEnabled && enabledHarborHostCount === 1;\n\n\tconst handleToggle = async ( checked: boolean ) => {\n\t\tif ( ! checked && isLastHarborHost ) {\n\t\t\tsetShowDeactivateConfirm( true );\n\t\t\treturn;\n\t\t}\n\n\t\tsetPendingAction( checked ? featureInstalled ? 'enabling' : 'installing' : 'disabling' );\n\t\tif ( checked ) {\n\t\t\tconst result = await enableFeature( feature.slug );\n\t\t\tif ( result instanceof HarborError ) {\n\t\t\t\taddError( result );\n\t\t\t} else {\n\t\t\t\taddToast(\n\t\t\t\t\t/* translators: %s is the name of the feature being enabled */\n\t\t\t\t\tsprintf( __( '%s enabled', '%TEXTDOMAIN%' ), feature.name ),\n\t\t\t\t\t'success',\n\t\t\t\t);\n\t\t\t\tsetNeedsReload( true );\n\t\t\t}\n\t\t} else {\n\t\t\tconst result = await disableFeature( feature.slug );\n\t\t\tif ( result instanceof HarborError ) {\n\t\t\t\taddError( result );\n\t\t\t} else {\n\t\t\t\taddToast(\n\t\t\t\t\t/* translators: %s is the name of the feature being disabled */\n\t\t\t\t\tsprintf( __( '%s disabled', '%TEXTDOMAIN%' ), feature.name ),\n\t\t\t\t\t'default',\n\t\t\t\t);\n\t\t\t\tsetNeedsReload( true );\n\t\t\t}\n\t\t}\n\t\tsetPendingAction( null );\n\t};\n\n\tconst handleConfirmDeactivate = async () => {\n\t\tsetShowDeactivateConfirm( false );\n\t\tsetPendingAction( 'disabling' );\n\t\tconst result = await disableFeature( feature.slug );\n\t\tif ( result instanceof HarborError ) {\n\t\t\taddError( result );\n\t\t} else {\n\t\t\twindow.location.href = window.harborData?.pluginsUrl ?? '/wp-admin/plugins.php';\n\t\t}\n\t\tsetPendingAction( null );\n\t};\n\n\tconst handleCancelDeactivate = () => {\n\t\tsetShowDeactivateConfirm( false );\n\t};\n\n\tconst handleUpdate = async () => {\n\t\tsetPendingAction( 'updating' );\n\t\tconst result = await updateFeature( feature.slug );\n\t\tif ( result instanceof HarborError ) {\n\t\t\taddError( result );\n\t\t} else {\n\t\t\t/* translators: %s is the name of the feature being updated */\n\t\t\taddToast( sprintf( __( '%s updated.', '%TEXTDOMAIN%' ), feature.name ), 'success' );\n\t\t}\n\t\tsetPendingAction( null );\n\t};\n\n\tconst badgeStatus = getBadgeStatus( pendingAction, licenseBadgeType, featureEnabled );\n\tconst showSwitch = pendingAction !== 'installing' && pendingAction !== 'updating';\n\tconst switchChecked = getSwitchChecked( pendingAction, featureEnabled );\n\n\treturn {\n\t\tpendingAction,\n\t\tinstallableBusy,\n\t\tbadgeStatus,\n\t\tshowSwitch,\n\t\tswitchChecked,\n\t\tlicenseBadgeType,\n\t\tshowDeactivateConfirm,\n\t\thandleToggle,\n\t\thandleUpdate,\n\t\thandleConfirmDeactivate,\n\t\thandleCancelDeactivate,\n\t};\n}\n","/**\n * Hook that returns features for a product filtered by the active\n * search query from FilterContext.\n *\n * When the search query is empty the original selector result is returned\n * directly, so ProductSection only re-renders when features or the query\n * actually change.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { useSelect } from '@wordpress/data';\nimport { useFilter } from '@/context/filter-context';\nimport { store as harborStore } from '@/store';\nimport type { Feature } from '@/types/api';\n\n/**\n * @since 1.0.0\n */\nexport function useFilteredFeatures( productSlug: string ): Feature[] {\n const { searchQuery } = useFilter();\n\n const features = useSelect(\n ( select ) => select( harborStore ).getFeaturesByProduct( productSlug ),\n [ productSlug ],\n );\n\n const query = searchQuery.trim();\n\n if ( ! query ) return features;\n\n // Try to use the query as a regex; fall back to a literal match if invalid.\n let pattern: RegExp;\n try {\n pattern = new RegExp( query, 'i' );\n } catch {\n pattern = new RegExp( query.replace( /[.*+?^${}()|[\\]\\\\]/g, '\\\\$&' ), 'i' );\n }\n\n return features.filter(\n ( f ) =>\n pattern.test( f.name ) ||\n pattern.test( f.slug ) ||\n pattern.test( f.description ),\n );\n}\n","/**\n * Partitions features for a product into available and locked groups,\n * and groups locked features by catalog tier.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { useMemo } from 'react';\nimport { useSelect } from '@wordpress/data';\nimport { useFilteredFeatures } from '@/hooks/useFilteredFeatures';\nimport { store as harborStore } from '@/store';\nimport { isFreeFeature, getFeatureMismatch } from '@/lib/feature-utils';\nimport type { CatalogTier, Feature } from '@/types/api';\n\nexport interface FeatureGroups {\n availableFeatures: Feature[];\n lockedByTier: Record;\n sortedCatalogTiers: CatalogTier[]; // All tiers — used for header tier name lookup\n upgradeCatalogTiers: CatalogTier[]; // Tiers strictly above the user's rank — upgrade CTA shown\n activationCatalogTiers: CatalogTier[]; // Tiers within the user's rank, locked only because not activated — no upgrade CTA\n isUnactivatedLicense: boolean; // true when the user owns the tier but has not activated it on this domain\n}\n\n/**\n * @since TBD Detect unactivated license products and route their tiers to activationCatalogTiers instead of upgradeCatalogTiers. Exposes isUnactivatedLicense flag.\n * @since 1.0.0\n */\nexport function useProductFeatureGroups( productSlug: string ): FeatureGroups {\n const allFeatures = useFilteredFeatures( productSlug );\n\n const { catalogTiers, licenseProducts, isLicenseValid, legacyLicenses, unactivatedLicenseProduct } = useSelect(\n ( select ) => ({\n catalogTiers: select( harborStore ).getProductCatalog( productSlug )?.tiers ?? [],\n licenseProducts: select( harborStore ).getLicenseProducts(),\n isLicenseValid: select( harborStore ).isProductLicenseValid( productSlug ),\n legacyLicenses: select( harborStore ).getLegacyLicenses(),\n unactivatedLicenseProduct: select( harborStore ).getUnactivatedLicenseProduct( productSlug ),\n }),\n [ productSlug ]\n );\n\n return useMemo( () => {\n const sorted = catalogTiers.slice().sort( ( a, b ) => a.rank - b.rank );\n const forProduct = licenseProducts.filter( ( lp ) => lp.product_slug === productSlug );\n const licenseProduct = forProduct.find( ( lp ) => lp.activated_here === true );\n\n // A license is \"invalid\" when a validation status is known but not 'valid'\n // (e.g. not_activated, expired, suspended). The raw tier is still present on\n // the product, but features are locked — the user needs to activate, not upgrade.\n const isLicenseInvalid = licenseProduct !== undefined &&\n licenseProduct.validation_status !== null &&\n licenseProduct.validation_status !== 'valid';\n\n // Rank of the activated product, or -1 when none is present.\n const activatedTier = licenseProduct?.tier\n ? sorted.find( ( t ) => t.tier_slug === licenseProduct.tier )\n : null;\n const activatedRank = activatedTier?.rank ?? -1;\n\n // Rank of the unactivated product, if any.\n // An unactivated product may sit above the activated tier (e.g. user purchased an\n // upgrade to elite while pro is already active but elite not yet activated on this\n // domain). In that case both tiers exist and only elite needs the activation badge.\n const unactivatedTier = unactivatedLicenseProduct?.tier\n ? sorted.find( ( t ) => t.tier_slug === unactivatedLicenseProduct.tier )\n : null;\n const unactivatedRank = unactivatedTier?.rank ?? -1;\n\n // isUnactivatedLicense: the user owns a tier they have not activated on this domain,\n // whether because no activated product exists at all (unactivatedRank > -1 > activatedRank)\n // or because a higher purchased tier is not yet activated (unactivatedRank > activatedRank).\n const isUnactivatedLicense = unactivatedLicenseProduct !== null && unactivatedRank > activatedRank;\n\n // Effective rank = highest owned tier (activated or not).\n const rank = Math.max( activatedRank, unactivatedRank );\n\n // Tiers strictly above the highest owned rank: features here need an upgrade.\n const upgrade = sorted.filter( ( t ) => t.rank > rank );\n\n // activationTiers covers two cases:\n // 1. isLicenseInvalid: tiers within the activated rank locked due to invalid status\n // (expired, suspended, etc.) — user needs to fix the license, not upgrade.\n // 2. isUnactivatedLicense: tiers above the activated rank but within the unactivated\n // rank — user owns them but hasn't activated on this domain yet.\n // Both render without an upgrade button.\n const activationTiers = isLicenseInvalid\n ? sorted.filter( ( t ) => t.rank <= activatedRank && t.rank > 0 )\n : isUnactivatedLicense\n ? sorted.filter( ( t ) => t.rank <= rank && t.rank > activatedRank )\n : [];\n const slugs = isLicenseValid\n ? new Set()\n : new Set( legacyLicenses.filter( ( l ) => l.is_active ).map( ( l ) => l.slug ) );\n\n const isLegacyAvailable = ( f: Feature ) => slugs.has( f.slug );\n\n // Available: the standard set, PLUS revoked features.\n // Revoked features are in the user's tier but have had their capability removed.\n // They render as disabled rows in the available section (not in upgrade accordions),\n // since the user does not need to upgrade — the tier already covers them.\n const availableFeatures = allFeatures.filter( ( f ) =>\n f.is_available ||\n isFreeFeature( f.tier ) ||\n isLegacyAvailable( f ) ||\n getFeatureMismatch( f ) === 'revoked'\n );\n\n // Locked: not available, not free, not legacy, and not revoked.\n const lockedFeatures = allFeatures.filter( ( f ) =>\n ! f.is_available &&\n ! isFreeFeature( f.tier ) &&\n ! isLegacyAvailable( f ) &&\n getFeatureMismatch( f ) !== 'revoked'\n );\n\n const lockedByTier = sorted.reduce>(\n ( acc, tier ) => {\n acc[ tier.tier_slug ] = lockedFeatures.filter( ( f ) => f.tier === tier.tier_slug );\n return acc;\n },\n {}\n );\n\n return {\n availableFeatures,\n lockedByTier,\n sortedCatalogTiers: sorted,\n upgradeCatalogTiers: upgrade,\n activationCatalogTiers: activationTiers,\n isUnactivatedLicense,\n };\n }, [ allFeatures, catalogTiers, licenseProducts, isLicenseValid, legacyLicenses, productSlug, unactivatedLicenseProduct ] );\n}\n","/**\n * Appends product and tier params to the base activation URL supplied by the\n * API, producing a product-scoped URL the Liquid Web portal can use to\n * pre-select the right product and tier.\n *\n * The base URL is already fully assembled by the server and includes params\n * such as portal-referral, redirect_url (percent-encoded), refresh, and\n * domain. This function only adds the two params it owns and never touches\n * the others.\n *\n * Example base URL from the API:\n * https://my.liquidweb.com/subscriptions/?portal-referral=plugin\n * &redirect_url=https%3A%2F%2Fexample.com%2Fwp-admin%2Fadmin.php%3Fpage%3Dlw-software-manager%26refresh%3Dauto\n * &domain=example.com\n *\n * @param baseUrl The raw activationUrl string from the API.\n * @param productSlug e.g. \"givewp\"\n * @param tier e.g. \"elite\"\n *\n * @since 1.0.0\n */\nexport function buildActivationUrl(\n baseUrl: string,\n productSlug: string,\n tier: string,\n): string {\n try {\n const url = new URL( baseUrl );\n url.searchParams.set( 'sku', `${ productSlug }:${ tier }` );\n return url.toString();\n } catch {\n return baseUrl;\n }\n}\n","/**\n * Builds a Commerce Portal change-plan URL for an existing subscription.\n *\n * Used when an upgrade CTA needs to drive a licensed customer to their\n * existing subscription's change-plan flow, rather than adding a brand-new\n * plan to the basket via the catalog's purchase_url.\n *\n * The portal resolves the subscription from the authenticated session, so\n * only the product and tier slugs appear in the path.\n *\n * Example:\n * base = https://my.software.stellarwp.com/subscriptions/\n * productSlug = kadence\n * tierSlug = pro\n * → https://my.software.stellarwp.com/subscriptions/kadence/pro/change-plan/\n *\n * @param baseUrl The subscriptionsUrl string from window.harborData. May\n * include a trailing slash and query string.\n * @param productSlug e.g. \"kadence\"\n * @param tierSlug e.g. \"pro\"\n *\n * @since 1.0.0\n */\nexport function buildChangePlanUrl(\n baseUrl: string,\n productSlug: string,\n tierSlug: string,\n): string {\n try {\n const url = new URL( baseUrl );\n const prefix = url.pathname.endsWith( '/' ) ? url.pathname : `${ url.pathname }/`;\n url.pathname = `${ prefix }${ encodeURIComponent( productSlug ) }/${ encodeURIComponent( tierSlug ) }/change-plan/`;\n return url.toString();\n } catch {\n return baseUrl;\n }\n}\n","import type { Feature, FeatureMismatchType } from '@/types/api';\n\nexport type LicenseBadgeType = 'free' | 'legacy' | 'bonus' | 'revoked';\n\n/**\n * True when a feature requires no paid tier — either it has no tier at all\n * or its tier slug contains \"free\" (e.g. \"give-free\").\n *\n * @since 1.0.0\n */\nexport function isFreeFeature( tier: string | null ): boolean {\n return ! tier || tier.toLowerCase().includes( 'free' );\n}\n\n/**\n * Returns the single license badge type to display for a feature row, or null if none applies.\n *\n * Enforces mutual exclusivity: only the first matching condition wins.\n *\n * @since 1.0.0\n */\nexport function getLicenseBadgeType( feature: Feature, isLegacy: boolean ): LicenseBadgeType | null {\n if ( isFreeFeature( feature.tier ) ) return 'free';\n if ( isLegacy ) return 'legacy';\n return getFeatureMismatch( feature );\n}\n\n/**\n * Returns the mismatch type for a feature, or null if there is no mismatch.\n *\n * Both fields are pre-computed by the backend resolution layer.\n * No catalog or license cross-referencing is needed at call sites.\n *\n * @since 1.0.0\n */\nexport function getFeatureMismatch( feature: Feature ): FeatureMismatchType {\n if ( feature.is_available && ! feature.in_catalog_tier ) {\n return 'bonus';\n }\n if ( ! feature.is_available && feature.in_catalog_tier ) {\n return 'revoked';\n }\n return null;\n}\n","/**\n * Utilities for @wordpress/data stores.\n *\n * Ported from sync-saas @utils/data/forward-resolver.js\n *\n * @package LiquidWeb\\Harbor\n */\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyResolver = ( ...args: any[] ) => any;\n\n/**\n * Forwards resolution to another resolver with the same arguments.\n *\n * Use when the source and target selectors share the same signature.\n */\nexport function forwardResolver( resolverName: string ): AnyResolver {\n\treturn ( ...args: unknown[] ) =>\n\t\tasync ( { resolveSelect }: Record ) => {\n\t\t\tawait resolveSelect[ resolverName ]( ...args );\n\t\t};\n}\n\n/**\n * Forwards resolution to another resolver, discarding arguments.\n *\n * Use when a derived selector (e.g. getFeature(slug)) depends on data\n * fetched by a resolver that takes no arguments (e.g. getFeatures()).\n */\nexport function forwardResolverWithoutArgs( resolverName: string ): AnyResolver {\n\treturn () =>\n\t\tasync ( { resolveSelect }: Record ) => {\n\t\t\tawait resolveSelect[ resolverName ]();\n\t\t};\n}\n","/**\n * @package LiquidWeb\\Harbor\n */\nimport { PRODUCTS } from '@/data/products';\nimport type { LicenseProduct } from '@/types/api';\n\nexport interface GroupedProduct {\n productSlug: string;\n productName: string;\n tiers: LicenseProduct[];\n}\n\n/**\n * Groups LicenseProduct entries by product_slug, sorts tiers within each group\n * (activated-here first, then ascending by rank), and orders groups by the\n * PRODUCTS constant. Products absent from licenseProducts are omitted.\n *\n * @since 1.0.0\n */\nexport function groupLicenseProducts(\n licenseProducts: LicenseProduct[],\n tierRankMap: Record,\n): GroupedProduct[] {\n const groups: Record = {};\n licenseProducts.forEach( ( lp ) => {\n if ( ! groups[ lp.product_slug ] ) {\n groups[ lp.product_slug ] = [];\n }\n groups[ lp.product_slug ].push( lp );\n } );\n\n Object.values( groups ).forEach( ( tiers ) => {\n tiers.sort( ( a, b ) => ( tierRankMap[ a.tier ] ?? 0 ) - ( tierRankMap[ b.tier ] ?? 0 ) );\n } );\n\n return PRODUCTS\n .filter( ( p ) => groups[ p.slug ] !== undefined )\n .map( ( p ) => ({ productSlug: p.slug, productName: p.name, tiers: groups[ p.slug ] }) );\n}\n","/**\n * Pure utility functions for license expiry display.\n *\n * @package LiquidWeb\\Harbor\n */\n\n/**\n * @since 1.0.0\n */\nexport function formatDate( dateStr: string ): string {\n return new Date( dateStr ).toLocaleDateString( 'en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n } );\n}\n\n/**\n * @since 1.0.0\n */\nexport function getExpiryStatus( dateStr: string ): 'expired' | 'expiring-soon' | 'ok' {\n const diff = new Date( dateStr ).getTime() - Date.now();\n if ( diff <= 0 ) return 'expired';\n if ( diff <= 30 * 24 * 60 * 60 * 1000 ) return 'expiring-soon';\n return 'ok';\n}\n\nexport const expiryTextClass: Record = {\n expired: 'text-destructive font-medium',\n 'expiring-soon': 'text-amber-600 font-medium',\n ok: 'text-muted-foreground',\n};\n","import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn( ...inputs: ClassValue[] ): string {\n return twMerge( clsx( inputs ) );\n}\n","/**\n * Action creators for the lw @wordpress/data store.\n *\n * @package LiquidWeb\\Harbor\n */\n\nimport apiFetch from '@wordpress/api-fetch';\nimport { __ } from '@wordpress/i18n';\nimport { HarborError, ErrorCode } from '@/errors';\nimport type { Feature, LegacyLicense, License, ProductCatalog } from '@/types/api';\nimport type { Action, Thunk } from './types';\n\n// ---------------------------------------------------------------------------\n// Plain action creators (synchronous)\n// ---------------------------------------------------------------------------\n\n/**\n * Receives the list of features from the REST API.\n *\n * @param features The list of features.\n * @since 1.0.0\n */\nexport const receiveFeatures = (features: Feature[]): Action => ({\n\ttype: 'RECEIVE_FEATURES',\n\tfeatures,\n});\n\n/**\n * Receives the list of Harbor host plugin basenames from the REST API.\n *\n * @param basenames The list of Harbor host plugin basenames.\n * @since 1.0.0\n */\nexport const receiveHarborHosts = (basenames: string[]): Action => ({\n\ttype: 'RECEIVE_HARBOR_HOSTS',\n\tbasenames,\n});\n\n/**\n * Receives the license from the REST API.\n *\n * @param license The license.\n * @since 1.0.0\n */\nexport const receiveLicense = (license: License): Action => ({\n\ttype: 'RECEIVE_LICENSE',\n\tlicense,\n});\n\n/**\n * Receives the product catalog from the REST API.\n *\n * @param catalogs The product catalog.\n * @since 1.0.0\n */\nexport const receiveCatalog = (catalogs: ProductCatalog[]): Action => ({\n\ttype: 'RECEIVE_CATALOG',\n\tcatalogs,\n});\n\n/**\n * Receives the legacy licenses from the REST API.\n *\n * @param licenses The legacy licenses.\n * @since 1.0.0\n */\nexport const receiveLegacyLicenses = (licenses: LegacyLicense[]): Action => ({\n\ttype: 'RECEIVE_LEGACY_LICENSES',\n\tlicenses,\n});\n\n// ---------------------------------------------------------------------------\n// Thunk action creators (async)\n// ---------------------------------------------------------------------------\n\n/**\n * Enable a feature via the REST API.\n *\n * @param slug\n * @since 1.0.0\n */\nexport const enableFeature =\n\t(slug: string): Thunk =>\n\tasync ({ dispatch }) => {\n\t\tdispatch({ type: 'TOGGLE_FEATURE_START', slug });\n\t\ttry {\n\t\t\tconst feature = await apiFetch({\n\t\t\t\tpath: `/liquidweb/harbor/v1/features/${slug}/enable`,\n\t\t\t\tmethod: 'POST',\n\t\t\t});\n\t\t\t// TOGGLE_FEATURE_FINISHED patches bySlug with the returned feature — no\n\t\t\t// need to invalidate getFeatures. A background re-fetch would race the\n\t\t\t// optimistic patch and could overwrite correct state with stale data,\n\t\t\t// causing the toggle flicker reproduced in https://github.com/stellarwp/harbor/pull/94.\n\t\t\tdispatch({ type: 'TOGGLE_FEATURE_FINISHED', feature });\n\t\t\t// Activation may have bootstrapped a new Harbor host plugin, so refresh\n\t\t\t// the hosts list. RECEIVE_HARBOR_HOSTS only touches harborHosts.basenames\n\t\t\t// and never overwrites bySlug, so there is no flicker risk.\n\t\t\tdispatch.invalidateResolution('getHarborHostBasenames', []);\n\t\t\treturn null;\n\t\t} catch (err) {\n\t\t\tconst error = await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.FeatureEnableFailed,\n\t\t\t\t__(\n\t\t\t\t\t'Liquid Web Software Manager failed to enable your feature.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t)\n\t\t\t);\n\t\t\tdispatch({ type: 'TOGGLE_FEATURE_FAILED', slug, error });\n\t\t\treturn error;\n\t\t}\n\t};\n\n/**\n * Disable a feature via the REST API.\n *\n * @param slug\n * @since 1.0.0\n */\nexport const disableFeature =\n\t(slug: string): Thunk =>\n\tasync ({ dispatch }) => {\n\t\tdispatch({ type: 'TOGGLE_FEATURE_START', slug });\n\t\ttry {\n\t\t\tconst feature = await apiFetch({\n\t\t\t\tpath: `/liquidweb/harbor/v1/features/${slug}/disable`,\n\t\t\t\tmethod: 'POST',\n\t\t\t});\n\t\t\t// Same reasoning as enableFeature: patch via TOGGLE_FEATURE_FINISHED,\n\t\t\t// do not invalidate getFeatures (https://github.com/stellarwp/harbor/pull/94). No hosts invalidation needed\n\t\t\t// because deactivation cannot introduce a new Harbor host.\n\t\t\tdispatch({ type: 'TOGGLE_FEATURE_FINISHED', feature });\n\t\t\treturn null;\n\t\t} catch (err) {\n\t\t\tconst error = await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.FeatureDisableFailed,\n\t\t\t\t__(\n\t\t\t\t\t'Liquid Web Software Manager failed to disable your feature.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t)\n\t\t\t);\n\t\t\tdispatch({ type: 'TOGGLE_FEATURE_FAILED', slug, error });\n\t\t\treturn error;\n\t\t}\n\t};\n\n/**\n * Update a feature via the REST API.\n *\n * @param slug\n * @since 1.0.0\n */\nexport const updateFeature =\n\t(slug: string): Thunk =>\n\tasync ({ dispatch }) => {\n\t\tdispatch({ type: 'UPDATE_FEATURE_START', slug });\n\t\ttry {\n\t\t\tconst feature = await apiFetch({\n\t\t\t\tpath: `/liquidweb/harbor/v1/features/${slug}/update`,\n\t\t\t\tmethod: 'POST',\n\t\t\t});\n\t\t\t// Same reasoning as enableFeature/disableFeature: UPDATE_FEATURE_FINISHED\n\t\t\t// patches bySlug directly — invalidating getFeatures is unnecessary and\n\t\t\t// risks the stale-overwrite flicker (https://github.com/stellarwp/harbor/pull/94).\n\t\t\tdispatch({ type: 'UPDATE_FEATURE_FINISHED', feature });\n\t\t\treturn null;\n\t\t} catch (err) {\n\t\t\tconst error = await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.FeatureUpdateFailed,\n\t\t\t\t__(\n\t\t\t\t\t'Liquid Web Software Manager failed to update your feature.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t)\n\t\t\t);\n\t\t\tdispatch({ type: 'UPDATE_FEATURE_FAILED', slug, error });\n\t\t\treturn error;\n\t\t}\n\t};\n\n/**\n * Store a license key via the REST API, then invalidate the license\n * and features resolvers so the UI refreshes with the new entitlements.\n *\n * @param key\n * @since 1.0.0\n */\nexport const storeLicense =\n\t(key: string): Thunk =>\n\tasync ({ dispatch, select }) => {\n\t\tif (!select.canModifyLicense()) {\n\t\t\treturn new HarborError(\n\t\t\t\tErrorCode.LicenseActionInProgress,\n\t\t\t\t__(\n\t\t\t\t\t'Liquid Web Software Manager failed to validate your license, another action is in progress.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t\tdispatch({ type: 'STORE_LICENSE_START' });\n\t\ttry {\n\t\t\tconst result = await apiFetch({\n\t\t\t\tpath: '/liquidweb/harbor/v1/license',\n\t\t\t\tmethod: 'POST',\n\t\t\t\tdata: { key },\n\t\t\t});\n\t\t\tdispatch({\n\t\t\t\ttype: 'STORE_LICENSE_FINISHED',\n\t\t\t\tlicense: result,\n\t\t\t});\n\t\t\t// License changes affect entitlements globally (available features, tiers,\n\t\t\t// locked state), so a full re-fetch of features is correct here — unlike\n\t\t\t// toggle/update actions where the API already returns the patched feature.\n\t\t\tdispatch.invalidateResolution('getFeatures', []);\n\t\t\treturn null;\n\t\t} catch (err) {\n\t\t\tconst error = await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.LicenseStoreFailed,\n\t\t\t\t__(\n\t\t\t\t\t'Liquid Web Software Manager failed to validate your license.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t)\n\t\t\t);\n\t\t\tdispatch({ type: 'STORE_LICENSE_FAILED', error });\n\t\t\treturn error;\n\t\t}\n\t};\n\n/**\n * Refresh the license from the upstream service via the REST API, then\n * invalidate the features resolver so the UI reflects any plan changes.\n *\n * @since 1.0.0\n */\nexport const refreshLicense =\n\t(): Thunk =>\n\tasync ({ dispatch, select }) => {\n\t\tif (!select.canModifyLicense()) {\n\t\t\treturn new HarborError(\n\t\t\t\tErrorCode.LicenseActionInProgress,\n\t\t\t\t__(\n\t\t\t\t\t'Liquid Web Software Manager failed to refresh your license, another action is in progress.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t\tdispatch({ type: 'REFRESH_LICENSE_START' });\n\t\ttry {\n\t\t\tconst result = await apiFetch({\n\t\t\t\tpath: '/liquidweb/harbor/v1/license/refresh',\n\t\t\t\tmethod: 'POST',\n\t\t\t});\n\t\t\tdispatch({ type: 'REFRESH_LICENSE_FINISHED', license: result });\n\t\t\tdispatch.invalidateResolution('getFeatures', []);\n\t\t\tif ( result.error ) {\n\t\t\t\treturn new HarborError( ErrorCode.LicenseValidateFailed, result.error.message );\n\t\t\t}\n\t\t\treturn null;\n\t\t} catch (err) {\n\t\t\tconst error = await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.LicenseRefreshFailed,\n\t\t\t\t__(\n\t\t\t\t\t'Liquid Web Software Manager failed to refresh your license.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t)\n\t\t\t);\n\t\t\tdispatch({ type: 'REFRESH_LICENSE_FAILED', error });\n\t\t\treturn error;\n\t\t}\n\t};\n\n/**\n * Refresh the product catalog from the upstream service via the REST API.\n *\n * @since 1.0.0\n */\nexport const refreshCatalog =\n\t(): Thunk =>\n\tasync ({ dispatch }) => {\n\t\ttry {\n\t\t\tconst result = await apiFetch({\n\t\t\t\tpath: '/liquidweb/harbor/v1/catalog/refresh',\n\t\t\t\tmethod: 'POST',\n\t\t\t});\n\t\t\tdispatch.receiveCatalog(result);\n\t\t\treturn null;\n\t\t} catch (err) {\n\t\t\tconst error = await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.CatalogRefreshFailed,\n\t\t\t\t__(\n\t\t\t\t\t'Liquid Web Software Manager failed to refresh the product catalog.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t)\n\t\t\t);\n\t\t\treturn error;\n\t\t}\n\t};\n\n/**\n * Delete the stored license key via the REST API, then invalidate the\n * features resolver so the UI refreshes.\n *\n * @since 1.0.0\n */\nexport const deleteLicense =\n\t(): Thunk =>\n\tasync ({ dispatch, select }) => {\n\t\tif (!select.canModifyLicense()) {\n\t\t\treturn new HarborError(\n\t\t\t\tErrorCode.LicenseActionInProgress,\n\t\t\t\t__(\n\t\t\t\t\t'Liquid Web Software Manager failed to delete your license, another action is in progress.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t\tdispatch({ type: 'DELETE_LICENSE_START' });\n\t\ttry {\n\t\t\tawait apiFetch({\n\t\t\t\tpath: '/liquidweb/harbor/v1/license',\n\t\t\t\tmethod: 'DELETE',\n\t\t\t});\n\t\t\tdispatch({ type: 'DELETE_LICENSE_FINISHED' });\n\t\t\tdispatch.invalidateResolution('getFeatures', []);\n\t\t\treturn null;\n\t\t} catch (err) {\n\t\t\tconst error = await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.LicenseDeleteFailed,\n\t\t\t\t__(\n\t\t\t\t\t'Liquid Web Software Manager failed to remove your license.',\n\t\t\t\t\t'%TEXTDOMAIN%'\n\t\t\t\t)\n\t\t\t);\n\t\t\tdispatch({ type: 'DELETE_LICENSE_FAILED', error });\n\t\t\treturn error;\n\t\t}\n\t};\n","/**\n * @wordpress/data store name for the Harbor library.\n *\n * @package LiquidWeb\\Harbor\n */\nexport const STORE_NAME = 'lw/harbor' as const;\n","/**\n * Registers the lw @wordpress/data store.\n *\n * Call registerHarborStore() once before createRoot() in index.tsx.\n * Consumers import the store descriptor and use useSelect / useDispatch.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { createReduxStore, register } from '@wordpress/data';\nimport { reducer } from './reducer';\nimport * as actions from './actions';\nimport * as selectors from './selectors';\nimport * as resolvers from './resolvers';\nimport { STORE_NAME } from './constants';\n\nexport const store = createReduxStore(STORE_NAME, {\n\treducer,\n\tactions,\n\tselectors,\n\tresolvers,\n});\n\nexport function registerHarborStore(): void {\n\tregister(store);\n}\n\nexport { STORE_NAME };\n","/**\n * Reducer for the lw @wordpress/data store.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { combineReducers } from '@wordpress/data';\nimport type {\n\tAction,\n\tCatalogState,\n\tFeaturesState,\n\tHarborHostsState,\n\tLegacyLicensesState,\n\tLicenseState,\n} from './types';\n\nexport const reducer = combineReducers({ features, harborHosts, license, catalog, legacyLicenses });\n\n// ---------------------------------------------------------------------------\n// Catalog\n// ---------------------------------------------------------------------------\n\nconst CATALOG_DEFAULT: CatalogState = {\n\tbyProductSlug: {},\n};\n\nfunction catalog(\n\tstate: CatalogState = CATALOG_DEFAULT,\n\taction: Action\n): CatalogState {\n\tswitch (action.type) {\n\t\tcase 'RECEIVE_CATALOG': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tbyProductSlug: Object.fromEntries(\n\t\t\t\t\taction.catalogs.map((c) => [c.product_slug, c])\n\t\t\t\t),\n\t\t\t};\n\t\t}\n\n\t\tdefault:\n\t\t\treturn state;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Legacy licenses\n// ---------------------------------------------------------------------------\n\nconst LEGACY_LICENSES_DEFAULT: LegacyLicensesState = {\n\tbySlug: {},\n};\n\nfunction legacyLicenses(\n\tstate: LegacyLicensesState = LEGACY_LICENSES_DEFAULT,\n\taction: Action\n): LegacyLicensesState {\n\tswitch (action.type) {\n\t\tcase 'RECEIVE_LEGACY_LICENSES': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tbySlug: Object.fromEntries(\n\t\t\t\t\taction.licenses.map((l) => [l.slug, l])\n\t\t\t\t),\n\t\t\t};\n\t\t}\n\n\t\tdefault:\n\t\t\treturn state;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Features\n// ---------------------------------------------------------------------------\n\nconst FEATURES_DEFAULT: FeaturesState = {\n\tbySlug: {},\n\ttoggling: {},\n\tupdating: {},\n\terrorBySlug: {},\n};\n\nfunction features(\n\tstate: FeaturesState = FEATURES_DEFAULT,\n\taction: Action\n): FeaturesState {\n\tswitch (action.type) {\n\t\tcase 'RECEIVE_FEATURES': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tbySlug: Object.fromEntries( action.features.map( ( f ) => [ f.slug, f ] ) ),\n\t\t\t};\n\t\t}\n\n\t\tcase 'TOGGLE_FEATURE_START': {\n\t\t\tconst { [action.slug]: _, ...remainingErrors } = state.errorBySlug;\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\ttoggling: { ...state.toggling, [action.slug]: true },\n\t\t\t\terrorBySlug: remainingErrors,\n\t\t\t};\n\t\t}\n\n\t\tcase 'TOGGLE_FEATURE_FINISHED': {\n\t\t\tconst { slug } = action.feature;\n\t\t\tconst { [slug]: _, ...remainingToggling } = state.toggling;\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tbySlug: { ...state.bySlug, [ slug ]: action.feature },\n\t\t\t\ttoggling: remainingToggling,\n\t\t\t};\n\t\t}\n\n\t\tcase 'TOGGLE_FEATURE_FAILED': {\n\t\t\tconst { [action.slug]: _, ...remainingToggling } = state.toggling;\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\ttoggling: remainingToggling,\n\t\t\t\terrorBySlug: {\n\t\t\t\t\t...state.errorBySlug,\n\t\t\t\t\t[action.slug]: action.error,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tcase 'UPDATE_FEATURE_START': {\n\t\t\tconst { [action.slug]: _, ...remainingErrors } = state.errorBySlug;\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tupdating: { ...state.updating, [action.slug]: true },\n\t\t\t\terrorBySlug: remainingErrors,\n\t\t\t};\n\t\t}\n\n\t\tcase 'UPDATE_FEATURE_FINISHED': {\n\t\t\tconst { slug } = action.feature;\n\t\t\tconst { [slug]: _, ...remainingUpdating } = state.updating;\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tbySlug: {\n\t\t\t\t\t...state.bySlug,\n\t\t\t\t\t[slug]: action.feature,\n\t\t\t\t},\n\t\t\t\tupdating: remainingUpdating,\n\t\t\t};\n\t\t}\n\n\t\tcase 'UPDATE_FEATURE_FAILED': {\n\t\t\tconst { [action.slug]: _, ...remainingUpdating } = state.updating;\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tupdating: remainingUpdating,\n\t\t\t\terrorBySlug: {\n\t\t\t\t\t...state.errorBySlug,\n\t\t\t\t\t[action.slug]: action.error,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tdefault:\n\t\t\treturn state;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Harbor hosts\n// ---------------------------------------------------------------------------\n\nconst HARBOR_HOSTS_DEFAULT: HarborHostsState = {\n\tbasenames: [],\n};\n\nfunction harborHosts(\n\tstate: HarborHostsState = HARBOR_HOSTS_DEFAULT,\n\taction: Action\n): HarborHostsState {\n\tswitch ( action.type ) {\n\t\tcase 'RECEIVE_HARBOR_HOSTS':\n\t\t\treturn { ...state, basenames: action.basenames };\n\t\tdefault:\n\t\t\treturn state;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// License\n// ---------------------------------------------------------------------------\n\nconst LICENSE_DEFAULT: LicenseState = {\n\tlicense: { key: null, products: [], error: null },\n\tisStoring: false,\n\tisDeleting: false,\n\tisRefreshing: false,\n\tstoreError: null,\n\tdeleteError: null,\n\trefreshError: null,\n};\n\nfunction license(\n\tstate: LicenseState = LICENSE_DEFAULT,\n\taction: Action\n): LicenseState {\n\tswitch (action.type) {\n\t\tcase 'RECEIVE_LICENSE': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tlicense: action.license,\n\t\t\t};\n\t\t}\n\n\t\tcase 'STORE_LICENSE_START': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisStoring: true,\n\t\t\t\tstoreError: null,\n\t\t\t};\n\t\t}\n\n\t\tcase 'STORE_LICENSE_FINISHED': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisStoring: false,\n\t\t\t\tlicense: action.license,\n\t\t\t};\n\t\t}\n\n\t\tcase 'STORE_LICENSE_FAILED': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisStoring: false,\n\t\t\t\tstoreError: action.error,\n\t\t\t};\n\t\t}\n\n\t\tcase 'DELETE_LICENSE_START': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisDeleting: true,\n\t\t\t\tdeleteError: null,\n\t\t\t};\n\t\t}\n\n\t\tcase 'DELETE_LICENSE_FINISHED': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisDeleting: false,\n\t\t\t\tlicense: { key: null, products: [], error: null },\n\t\t\t};\n\t\t}\n\n\t\tcase 'DELETE_LICENSE_FAILED': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisDeleting: false,\n\t\t\t\tdeleteError: action.error,\n\t\t\t};\n\t\t}\n\n\t\tcase 'REFRESH_LICENSE_START': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisRefreshing: true,\n\t\t\t\trefreshError: null,\n\t\t\t};\n\t\t}\n\n\t\tcase 'REFRESH_LICENSE_FINISHED': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisRefreshing: false,\n\t\t\t\tlicense: action.license,\n\t\t\t};\n\t\t}\n\n\t\tcase 'REFRESH_LICENSE_FAILED': {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisRefreshing: false,\n\t\t\t\trefreshError: action.error,\n\t\t\t};\n\t\t}\n\n\t\tdefault:\n\t\t\treturn state;\n\t}\n}\n","/**\n * Resolvers for the lw @wordpress/data store.\n *\n * Each resolver name matches a selector. @wordpress/data calls the resolver\n * automatically the first time the matching selector is invoked, then marks\n * it as resolved so subsequent calls hit the cache.\n *\n * @package LiquidWeb\\Harbor\n */\nimport apiFetch from '@wordpress/api-fetch';\nimport { __ } from '@wordpress/i18n';\nimport { HarborError, ErrorCode } from '@/errors';\nimport type { Feature, LegacyLicense, ProductCatalog, License } from '@/types/api';\nimport type { Thunk } from './types';\nimport { forwardResolver, forwardResolverWithoutArgs } from '@/lib/forward-resolver';\n\n/**\n * Fetches all features from the REST API and stores them.\n * Triggered automatically when getFeatures is first called.\n */\nexport const getFeatures =\n\t(): Thunk =>\n\tasync ({ dispatch }) => {\n\t\ttry {\n\t\t\tconst features = await apiFetch({\n\t\t\t\tpath: '/liquidweb/harbor/v1/features',\n\t\t\t});\n\t\t\tdispatch.receiveFeatures(features);\n\t\t} catch (err) {\n\t\t\tthrow await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.FeaturesFetchFailed,\n\t\t\t\t__('Liquid Web Software Manager failed to load your features.', '%TEXTDOMAIN%')\n\t\t\t);\n\t\t}\n\t};\n\n/**\n * Fetches the active Harbor host plugin basenames from the REST API.\n * Triggered automatically when getHarborHostBasenames is first called, and\n * invalidated after plugin activation so the list stays current.\n */\nexport const getHarborHostBasenames =\n\t(): Thunk =>\n\tasync ({ dispatch }) => {\n\t\ttry {\n\t\t\tconst basenames = await apiFetch({\n\t\t\t\tpath: '/liquidweb/harbor/v1/hosts',\n\t\t\t});\n\t\t\tdispatch.receiveHarborHosts(basenames);\n\t\t} catch (err) {\n\t\t\tthrow await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.FeaturesFetchFailed,\n\t\t\t\t__('Liquid Web Software Manager failed to load Harbor hosts.', '%TEXTDOMAIN%')\n\t\t\t);\n\t\t}\n\t};\n\nexport const getFeaturesByProduct = forwardResolverWithoutArgs('getFeatures');\nexport const getFeature = forwardResolverWithoutArgs('getFeatures');\nexport const isFeatureEnabled = forwardResolverWithoutArgs('getFeatures');\n\n// ---------------------------------------------------------------------------\n// Legacy licenses\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches legacy licenses from the REST API.\n */\nexport const getLegacyLicenses =\n\t(): Thunk =>\n\tasync ({ dispatch }) => {\n\t\ttry {\n\t\t\tconst licenses = await apiFetch({\n\t\t\t\tpath: '/liquidweb/harbor/v1/legacy-licenses',\n\t\t\t});\n\t\t\tdispatch.receiveLegacyLicenses(licenses);\n\t\t} catch (err) {\n\t\t\tthrow await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.LegacyLicensesFetchFailed,\n\t\t\t\t__('Liquid Web Software Manager failed to load legacy licenses.', '%TEXTDOMAIN%')\n\t\t\t);\n\t\t}\n\t};\n\nexport const getLegacyLicenseBySlug = forwardResolverWithoutArgs('getLegacyLicenses');\nexport const hasLegacyLicense = forwardResolverWithoutArgs('getLegacyLicenses');\nexport const hasLegacyLicenses = forwardResolver('getLegacyLicenses');\n\n// ---------------------------------------------------------------------------\n// Catalog\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches all product catalogs from the REST API and stores them.\n * Triggered automatically when getCatalog is first called.\n */\nexport const getCatalog =\n\t(): Thunk =>\n\tasync ({ dispatch }) => {\n\t\ttry {\n\t\t\tconst catalogs = await apiFetch({\n\t\t\t\tpath: '/liquidweb/harbor/v1/catalog',\n\t\t\t});\n\t\t\tdispatch.receiveCatalog(catalogs);\n\t\t} catch (err) {\n\t\t\tthrow await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.CatalogFetchFailed,\n\t\t\t\t__('Liquid Web Software Manager failed to load the product catalog.', '%TEXTDOMAIN%')\n\t\t\t);\n\t\t}\n\t};\n\nexport const getProductCatalog = forwardResolverWithoutArgs('getCatalog');\nexport const getProductTiers = forwardResolverWithoutArgs('getCatalog');\nexport const getCatalogTier = forwardResolverWithoutArgs('getCatalog');\n\n// ---------------------------------------------------------------------------\n// License\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches the stored license from the REST API.\n * Triggered automatically when getLicenseKey is first called.\n */\nexport const getLicenseKey =\n\t(): Thunk =>\n\tasync ({ dispatch }) => {\n\t\ttry {\n\t\t\tconst result = await apiFetch({\n\t\t\t\tpath: '/liquidweb/harbor/v1/license',\n\t\t\t});\n\t\t\tdispatch.receiveLicense(result);\n\t\t} catch (err) {\n\t\t\tthrow await HarborError.wrap(\n\t\t\t\terr,\n\t\t\t\tErrorCode.LicenseFetchFailed,\n\t\t\t\t__('Liquid Web Software Manager failed to load your license.', '%TEXTDOMAIN%')\n\t\t\t);\n\t\t}\n\t};\n\nexport const hasLicense = forwardResolver( 'getLicenseKey' );\nexport const getLicenseProducts = forwardResolverWithoutArgs( 'getLicenseKey' );\n","/**\n * Selectors for the lw @wordpress/data store.\n *\n * @package LiquidWeb\\Harbor\n */\nimport { createSelector } from '@wordpress/data';\nimport type { State } from './types';\nimport type {\n\tCatalogTier,\n\tFeature,\n\tFeatureMismatchType,\n\tLegacyLicense,\n\tLicenseError,\n\tLicenseProduct,\n\tProductCatalog,\n} from '@/types/api';\nimport type HarborError from '@/errors/harbor-error';\nimport { getFeatureMismatch } from '@/lib/feature-utils';\nimport { isInstallableFeature } from '@/types/utils';\n\n// ---------------------------------------------------------------------------\n// Features\n// ---------------------------------------------------------------------------\n\nexport const getFeatures = createSelector(\n\t(state: State): Feature[] => Object.values(state.features.bySlug),\n\t(state: State) => [state.features.bySlug]\n);\n\nexport const getFeaturesByProduct = createSelector(\n\t(state: State, product: string): Feature[] =>\n\t\tObject.values(state.features.bySlug).filter((f) => f.product === product),\n\t(state: State, product: string) => [state.features.bySlug, product]\n);\n\nexport const getFeature = (state: State, slug: string): Feature | null =>\n\tstate.features.bySlug[slug] ?? null;\n\nexport const isFeatureEnabled = (state: State, slug: string): boolean =>\n\tstate.features.bySlug[slug]?.is_enabled ?? false;\n\nexport const isFeatureToggling = (state: State, slug: string): boolean =>\n\tstate.features.toggling[slug] ?? false;\n\nexport const getFeatureError = (\n\tstate: State,\n\tslug: string\n): HarborError | null => state.features.errorBySlug[slug] ?? null;\n\n/**\n * Returns the capability mismatch type for a feature, or null if there is none.\n *\n * Wraps getFeatureMismatch() for consumers that only have access to the store.\n * Hooks that already hold a Feature object should call getFeatureMismatch() directly.\n */\nexport const getFeatureMismatchType = (\n\tstate: State,\n\tslug: string\n): FeatureMismatchType => {\n\tconst feature = state.features.bySlug[ slug ];\n\tif ( ! feature ) return null;\n\treturn getFeatureMismatch( feature );\n};\n\nexport const isFeatureUpdating = (state: State, slug: string): boolean =>\n\tstate.features.updating[slug] ?? false;\n\n/**\n * Returns the plugin basenames of all active Harbor-bundled plugins.\n */\nexport const getHarborHostBasenames = ( state: State ): string[] =>\n\tstate.harborHosts.basenames;\n\n/**\n * Returns the number of Harbor host plugins that are currently enabled.\n *\n * Uses the dedicated hosts registry (accurate after activation) rather than\n * the is_harbor_host field on features (unreliable for mid-request activations).\n */\nexport const getEnabledHarborHostCount = createSelector(\n\t( state: State ): number =>\n\t\tObject.values( state.features.bySlug ).filter(\n\t\t\t( f ) =>\n\t\t\t\tf.type === 'plugin' &&\n\t\t\t\tstate.harborHosts.basenames.includes( f.plugin_file ) &&\n\t\t\t\tf.is_enabled\n\t\t).length,\n\t( state: State ) => [ state.harborHosts.basenames, state.features.bySlug ]\n);\n\n/**\n * True when any feature is being toggled or updated.\n *\n * Both toggle and update operations trigger WordPress install/activate/deactivate\n * operations that should not run concurrently.\n *\n * Memoized via createSelector so the loops only re-run when\n * the relevant sub-trees actually change.\n */\nexport const isAnyInstallableBusy = createSelector(\n\t(state: State): boolean => {\n\t\tconst { toggling, updating, bySlug } = state.features;\n\t\tconst isInstallable = (slug: string): boolean => {\n\t\t\tconst feature = bySlug[slug];\n\t\t\treturn feature !== undefined && isInstallableFeature(feature);\n\t\t};\n\t\treturn (\n\t\t\tObject.keys(toggling).some(isInstallable) ||\n\t\t\tObject.keys(updating).some(isInstallable)\n\t\t);\n\t},\n\t(state: State) => [\n\t\tstate.features.toggling,\n\t\tstate.features.updating,\n\t\tstate.features.bySlug,\n\t]\n);\n\n// ---------------------------------------------------------------------------\n// Legacy licenses\n// ---------------------------------------------------------------------------\n\nexport const getLegacyLicenses = createSelector(\n\t(state: State): LegacyLicense[] => Object.values(state.legacyLicenses.bySlug),\n\t(state: State) => [state.legacyLicenses.bySlug]\n);\n\nexport const getLegacyLicenseBySlug = (state: State, slug: string): LegacyLicense | null =>\n\tstate.legacyLicenses.bySlug[slug] ?? null;\n\nexport const hasLegacyLicense = (state: State, slug: string): boolean =>\n\tslug in state.legacyLicenses.bySlug;\n\nexport const hasLegacyLicenses = (state: State): boolean =>\n\tObject.keys(state.legacyLicenses.bySlug).length > 0;\n\n/**\n * True when at least one legacy license belongs to a product not already\n * covered by the unified license. Used to suppress the banner when the\n * unified license makes legacy notices redundant.\n */\nexport const hasUncoveredLegacyLicenses = (state: State): boolean =>\n\tObject.values(state.legacyLicenses.bySlug).some(\n\t\t(license) => !isProductUnifiedLicensed(state, license.product)\n\t);\n\n/**\n * Returns the legacy license for the given feature slug only if it is active,\n * or null if it does not exist or has expired.\n */\nexport const getActiveLegacyLicense = (state: State, slug: string): LegacyLicense | null => {\n\tconst license = state.legacyLicenses.bySlug[ slug ] ?? null;\n\treturn license !== null && license.is_active ? license : null;\n};\n\n/**\n * Returns all license products excluding those with a cancelled status.\n *\n * Cancelled products are excluded globally so they do not influence any\n * selector's output — they clutter the UI and should not affect license\n * validity or activation checks.\n */\nexport const getWithoutCancelledProducts = ( state: State ): LicenseProduct[] =>\n\tstate.license.license.products.filter( ( p ) => p.status !== 'cancelled' );\n\n/**\n * True when the unified license covers the given product slug.\n */\nexport const isProductUnifiedLicensed = (state: State, productSlug: string): boolean =>\n\tgetWithoutCancelledProducts( state ).some( ( p ) => p.product_slug === productSlug );\n\n/**\n * True when any tier entry for the given product has is_valid set to true,\n * meaning the product is activated on the current domain with an active entitlement.\n *\n * @since 1.0.0\n */\nexport const isProductLicenseValid = ( state: State, productSlug: string ): boolean =>\n\tgetWithoutCancelledProducts( state ).some( ( p ) => p.product_slug === productSlug && p.is_valid === true );\n\n/**\n * True when at least one feature belonging to the product has an active legacy license.\n */\nexport const hasActiveLegacyLicenseForProduct = createSelector(\n\t(state: State, productSlug: string): boolean =>\n\t\tObject.values( state.features.bySlug )\n\t\t\t.filter( (f) => f.product === productSlug )\n\t\t\t.some( (f) => state.legacyLicenses.bySlug[ f.slug ]?.is_active === true ),\n\t(state: State, productSlug: string) => [ state.features.bySlug, state.legacyLicenses.bySlug, productSlug ]\n);\n\n// ---------------------------------------------------------------------------\n// Catalog\n// ---------------------------------------------------------------------------\n\nexport const getCatalog = createSelector(\n\t(state: State): ProductCatalog[] =>\n\t\tObject.values(state.catalog.byProductSlug),\n\t(state: State) => [state.catalog.byProductSlug]\n);\n\nexport const getProductCatalog = (\n\tstate: State,\n\tslug: string\n): ProductCatalog | null => state.catalog.byProductSlug[slug] ?? null;\n\nexport const getProductTiers = createSelector(\n\t(state: State, slug: string): CatalogTier[] =>\n\t\tstate.catalog.byProductSlug[slug]?.tiers ?? [],\n\t(state: State, slug: string) => [state.catalog.byProductSlug, slug]\n);\n\n/**\n * Returns a single CatalogTier by product slug and tier slug, or null.\n *\n * Returns the full tier object so callers can read price, currency, etc.\n */\nexport const getCatalogTier = (\n\tstate: State,\n\tproductSlug: string,\n\ttierSlug: string\n): CatalogTier | null =>\n\tstate.catalog.byProductSlug[productSlug]?.tiers.find(\n\t\t(t) => t.tier_slug === tierSlug\n\t) ?? null;\n\n// ---------------------------------------------------------------------------\n// License\n// ---------------------------------------------------------------------------\n\nconst UNACTIVATED_STATUSES = [ 'not_activated', 'activation_required' ] as const;\n\n/**\n * True when a license is present and every non-expired product's validation_status\n * indicates it has not been activated on this domain. Expired products are excluded\n * so they don't suppress the notice for products that can still be activated.\n */\nexport const areAllProductsNotActivated = ( state: State ): boolean => {\n\tconst products = getWithoutCancelledProducts( state ).filter(\n\t\t( p ) => p.validation_status !== 'expired'\n\t);\n\treturn (\n\t\tproducts.length > 0 &&\n\t\tproducts.every(\n\t\t\t( p ) => UNACTIVATED_STATUSES.includes( p.validation_status as typeof UNACTIVATED_STATUSES[number] )\n\t\t)\n\t);\n};\n\n/**\n * Returns the first license product for the given slug that the user owns but\n * has not yet activated on this domain, or null when none exists.\n *\n * Matches entries where activated_here is not true and validation_status is\n * not_activated or activation_required — i.e. the subscription exists but the\n * current domain is not in the activations list.\n *\n * @since TBD\n */\nexport const getUnactivatedLicenseProduct = (\n\tstate: State,\n\tproductSlug: string\n): LicenseProduct | null =>\n\tgetWithoutCancelledProducts( state ).find(\n\t\t( p ) =>\n\t\t\tp.product_slug === productSlug &&\n\t\t\tp.activated_here !== true &&\n\t\t\tUNACTIVATED_STATUSES.includes( p.validation_status as typeof UNACTIVATED_STATUSES[ number ] )\n\t) ?? null;\n\n/**\n * Returns the stored unified license key, or null. Triggers getLicenseKey resolver.\n * @param state\n */\nexport const getLicenseKey = (state: State): string | null =>\n\tstate.license.license.key;\n\nexport const hasLicense = (state: State): boolean =>\n\tstate.license.license.key !== null;\n\nexport const getLicenseProducts = (state: State): LicenseProduct[] =>\n\tgetWithoutCancelledProducts( state )\n\t\t.slice()\n\t\t.sort( ( a, b ) => ( b.activated_here === true ? 1 : 0 ) - ( a.activated_here === true ? 1 : 0 ) );\n\nexport const getLicenseError = (state: State): LicenseError | null =>\n\tstate.license.license.error;\n\nexport const isLicenseStoring = (state: State): boolean =>\n\tstate.license.isStoring;\n\nexport const isLicenseDeleting = (state: State): boolean =>\n\tstate.license.isDeleting;\n\nexport const isLicenseRefreshing = (state: State): boolean =>\n\tstate.license.isRefreshing;\n\nexport const canModifyLicense = (state: State): boolean =>\n\t!state.license.isStoring &&\n\t!state.license.isDeleting &&\n\t!state.license.isRefreshing;\n\nexport const getStoreLicenseError = (state: State): HarborError | null =>\n\tstate.license.storeError;\n\nexport const getDeleteLicenseError = (state: State): HarborError | null =>\n\tstate.license.deleteError;\n\nexport const getRefreshLicenseError = (state: State): HarborError | null =>\n\tstate.license.refreshError;\n","/**\n * Type guard utilities for narrowing Feature union types.\n *\n * @package LiquidWeb\\Harbor\n */\nimport type {\n\tFeature,\n\tPluginFeature,\n\tThemeFeature,\n\tServiceFeature,\n\tInstallableFeature,\n} from '@/types/api';\n\nexport function isPluginFeature( feature: Feature ): feature is PluginFeature {\n\treturn feature.type === 'plugin';\n}\n\nexport function isThemeFeature( feature: Feature ): feature is ThemeFeature {\n\treturn feature.type === 'theme';\n}\n\nexport function isServiceFeature( feature: Feature ): feature is ServiceFeature {\n\treturn feature.type === 'service';\n}\n\nexport function isInstallableFeature( feature: Feature ): feature is InstallableFeature {\n\treturn feature.type === 'plugin' || feature.type === 'theme';\n}\n","var currentNonce;\nexport var setNonce = function (nonce) {\n currentNonce = nonce;\n};\nexport var getNonce = function () {\n if (currentNonce) {\n return currentNonce;\n }\n if (typeof __webpack_nonce__ !== 'undefined') {\n return __webpack_nonce__;\n }\n return undefined;\n};\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport { forwardRef, createElement } from 'react';\nimport defaultAttributes from './defaultAttributes.js';\nimport { hasA11yProp } from './shared/src/utils/hasA11yProp.js';\nimport { mergeClasses } from './shared/src/utils/mergeClasses.js';\n\nconst Icon = forwardRef(\n ({\n color = \"currentColor\",\n size = 24,\n strokeWidth = 2,\n absoluteStrokeWidth,\n className = \"\",\n children,\n iconNode,\n ...rest\n }, ref) => createElement(\n \"svg\",\n {\n ref,\n ...defaultAttributes,\n width: size,\n height: size,\n stroke: color,\n strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,\n className: mergeClasses(\"lucide\", className),\n ...!children && !hasA11yProp(rest) && { \"aria-hidden\": \"true\" },\n ...rest\n },\n [\n ...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),\n ...Array.isArray(children) ? children : [children]\n ]\n )\n);\n\nexport { Icon as default };\n//# sourceMappingURL=Icon.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport { forwardRef, createElement } from 'react';\nimport { mergeClasses } from './shared/src/utils/mergeClasses.js';\nimport { toKebabCase } from './shared/src/utils/toKebabCase.js';\nimport { toPascalCase } from './shared/src/utils/toPascalCase.js';\nimport Icon from './Icon.js';\n\nconst createLucideIcon = (iconName, iconNode) => {\n const Component = forwardRef(\n ({ className, ...props }, ref) => createElement(Icon, {\n ref,\n iconNode,\n className: mergeClasses(\n `lucide-${toKebabCase(toPascalCase(iconName))}`,\n `lucide-${iconName}`,\n className\n ),\n ...props\n })\n );\n Component.displayName = toPascalCase(iconName);\n return Component;\n};\n\nexport { createLucideIcon as default };\n//# sourceMappingURL=createLucideIcon.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nvar defaultAttributes = {\n xmlns: \"http://www.w3.org/2000/svg\",\n width: 24,\n height: 24,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 2,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n};\n\nexport { defaultAttributes as default };\n//# sourceMappingURL=defaultAttributes.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [[\"path\", { d: \"M20 6 9 17l-5-5\", key: \"1gmf2c\" }]];\nconst Check = createLucideIcon(\"check\", __iconNode);\n\nexport { __iconNode, Check as default };\n//# sourceMappingURL=check.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [[\"path\", { d: \"m6 9 6 6 6-6\", key: \"qrunsl\" }]];\nconst ChevronDown = createLucideIcon(\"chevron-down\", __iconNode);\n\nexport { __iconNode, ChevronDown as default };\n//# sourceMappingURL=chevron-down.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [[\"path\", { d: \"m9 18 6-6-6-6\", key: \"mthhwq\" }]];\nconst ChevronRight = createLucideIcon(\"chevron-right\", __iconNode);\n\nexport { __iconNode, ChevronRight as default };\n//# sourceMappingURL=chevron-right.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [[\"path\", { d: \"m18 15-6-6-6 6\", key: \"153udz\" }]];\nconst ChevronUp = createLucideIcon(\"chevron-up\", __iconNode);\n\nexport { __iconNode, ChevronUp as default };\n//# sourceMappingURL=chevron-up.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"M21.801 10A10 10 0 1 1 17 3.335\", key: \"yps3ct\" }],\n [\"path\", { d: \"m9 11 3 3L22 4\", key: \"1pflzl\" }]\n];\nconst CircleCheckBig = createLucideIcon(\"circle-check-big\", __iconNode);\n\nexport { __iconNode, CircleCheckBig as default };\n//# sourceMappingURL=circle-check-big.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"M12 15V3\", key: \"m9g1x1\" }],\n [\"path\", { d: \"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\", key: \"ih7n3h\" }],\n [\"path\", { d: \"m7 10 5 5 5-5\", key: \"brsn70\" }]\n];\nconst Download = createLucideIcon(\"download\", __iconNode);\n\nexport { __iconNode, Download as default };\n//# sourceMappingURL=download.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"M15 3h6v6\", key: \"1q9fwt\" }],\n [\"path\", { d: \"M10 14 21 3\", key: \"gplh6r\" }],\n [\"path\", { d: \"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\", key: \"a6xqqp\" }]\n];\nconst ExternalLink = createLucideIcon(\"external-link\", __iconNode);\n\nexport { __iconNode, ExternalLink as default };\n//# sourceMappingURL=external-link.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"circle\", { cx: \"12\", cy: \"12\", r: \"10\", key: \"1mglay\" }],\n [\"path\", { d: \"M12 16v-4\", key: \"1dtifu\" }],\n [\"path\", { d: \"M12 8h.01\", key: \"e9boi3\" }]\n];\nconst Info = createLucideIcon(\"info\", __iconNode);\n\nexport { __iconNode, Info as default };\n//# sourceMappingURL=info.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\n \"path\",\n {\n d: \"M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z\",\n key: \"1s6t7t\"\n }\n ],\n [\"circle\", { cx: \"16.5\", cy: \"7.5\", r: \".5\", fill: \"currentColor\", key: \"w0ekpg\" }]\n];\nconst KeyRound = createLucideIcon(\"key-round\", __iconNode);\n\nexport { __iconNode, KeyRound as default };\n//# sourceMappingURL=key-round.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [[\"path\", { d: \"M21 12a9 9 0 1 1-6.219-8.56\", key: \"13zald\" }]];\nconst LoaderCircle = createLucideIcon(\"loader-circle\", __iconNode);\n\nexport { __iconNode, LoaderCircle as default };\n//# sourceMappingURL=loader-circle.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"rect\", { width: \"18\", height: \"11\", x: \"3\", y: \"11\", rx: \"2\", ry: \"2\", key: \"1w4ew1\" }],\n [\"path\", { d: \"M7 11V7a5 5 0 0 1 10 0v4\", key: \"fwvmzm\" }]\n];\nconst Lock = createLucideIcon(\"lock\", __iconNode);\n\nexport { __iconNode, Lock as default };\n//# sourceMappingURL=lock.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7\", key: \"132q7q\" }],\n [\"rect\", { x: \"2\", y: \"4\", width: \"20\", height: \"16\", rx: \"2\", key: \"izxlao\" }]\n];\nconst Mail = createLucideIcon(\"mail\", __iconNode);\n\nexport { __iconNode, Mail as default };\n//# sourceMappingURL=mail.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\n \"path\",\n {\n d: \"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\",\n key: \"1a8usu\"\n }\n ],\n [\"path\", { d: \"m15 5 4 4\", key: \"1mk7zo\" }]\n];\nconst Pencil = createLucideIcon(\"pencil\", __iconNode);\n\nexport { __iconNode, Pencil as default };\n//# sourceMappingURL=pencil.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\", key: \"v9h5vc\" }],\n [\"path\", { d: \"M21 3v5h-5\", key: \"1q7to0\" }],\n [\"path\", { d: \"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\", key: \"3uifl3\" }],\n [\"path\", { d: \"M8 16H3v5\", key: \"1cv678\" }]\n];\nconst RefreshCw = createLucideIcon(\"refresh-cw\", __iconNode);\n\nexport { __iconNode, RefreshCw as default };\n//# sourceMappingURL=refresh-cw.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5\", key: \"qeys4\" }],\n [\n \"path\",\n {\n d: \"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09\",\n key: \"u4xsad\"\n }\n ],\n [\n \"path\",\n {\n d: \"M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z\",\n key: \"676m9\"\n }\n ],\n [\"path\", { d: \"M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05\", key: \"92ym6u\" }]\n];\nconst Rocket = createLucideIcon(\"rocket\", __iconNode);\n\nexport { __iconNode, Rocket as default };\n//# sourceMappingURL=rocket.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"m21 21-4.34-4.34\", key: \"14j7rj\" }],\n [\"circle\", { cx: \"11\", cy: \"11\", r: \"8\", key: \"4ej97u\" }]\n];\nconst Search = createLucideIcon(\"search\", __iconNode);\n\nexport { __iconNode, Search as default };\n//# sourceMappingURL=search.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"M10 11v6\", key: \"nco0om\" }],\n [\"path\", { d: \"M14 11v6\", key: \"outv1u\" }],\n [\"path\", { d: \"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\", key: \"miytrc\" }],\n [\"path\", { d: \"M3 6h18\", key: \"d0wm0j\" }],\n [\"path\", { d: \"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\", key: \"e791ji\" }]\n];\nconst Trash2 = createLucideIcon(\"trash-2\", __iconNode);\n\nexport { __iconNode, Trash2 as default };\n//# sourceMappingURL=trash-2.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\n \"path\",\n {\n d: \"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\",\n key: \"wmoenq\"\n }\n ],\n [\"path\", { d: \"M12 9v4\", key: \"juzpu7\" }],\n [\"path\", { d: \"M12 17h.01\", key: \"p32p05\" }]\n];\nconst TriangleAlert = createLucideIcon(\"triangle-alert\", __iconNode);\n\nexport { __iconNode, TriangleAlert as default };\n//# sourceMappingURL=triangle-alert.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst __iconNode = [\n [\"path\", { d: \"M18 6 6 18\", key: \"1bl5f8\" }],\n [\"path\", { d: \"m6 6 12 12\", key: \"d8bk6v\" }]\n];\nconst X = createLucideIcon(\"x\", __iconNode);\n\nexport { __iconNode, X as default };\n//# sourceMappingURL=x.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nconst hasA11yProp = (props) => {\n for (const prop in props) {\n if (prop.startsWith(\"aria-\") || prop === \"role\" || prop === \"title\") {\n return true;\n }\n }\n return false;\n};\n\nexport { hasA11yProp };\n//# sourceMappingURL=hasA11yProp.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nconst mergeClasses = (...classes) => classes.filter((className, index, array) => {\n return Boolean(className) && className.trim() !== \"\" && array.indexOf(className) === index;\n}).join(\" \").trim();\n\nexport { mergeClasses };\n//# sourceMappingURL=mergeClasses.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nconst toCamelCase = (string) => string.replace(\n /^([A-Z])|[\\s-_]+(\\w)/g,\n (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase()\n);\n\nexport { toCamelCase };\n//# sourceMappingURL=toCamelCase.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nconst toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase();\n\nexport { toKebabCase };\n//# sourceMappingURL=toKebabCase.js.map\n","/**\n * @license lucide-react v0.575.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport { toCamelCase } from './toCamelCase.js';\n\nconst toPascalCase = (string) => {\n const camelCase = toCamelCase(string);\n return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);\n};\n\nexport { toPascalCase };\n//# sourceMappingURL=toPascalCase.js.map\n","// extracted by mini-css-extract-plugin\nexport {};","'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n","import * as React from 'react';\nimport { styleSingleton } from 'react-style-singleton';\nimport { fullWidthClassName, zeroRightClassName, noScrollbarsClassName, removedBarSizeVariable } from './constants';\nimport { getGapWidth } from './utils';\nvar Style = styleSingleton();\nexport var lockAttribute = 'data-scroll-locked';\n// important tip - once we measure scrollBar width and remove them\n// we could not repeat this operation\n// thus we are using style-singleton - only the first \"yet correct\" style will be applied.\nvar getStyles = function (_a, allowRelative, gapMode, important) {\n var left = _a.left, top = _a.top, right = _a.right, gap = _a.gap;\n if (gapMode === void 0) { gapMode = 'margin'; }\n return \"\\n .\".concat(noScrollbarsClassName, \" {\\n overflow: hidden \").concat(important, \";\\n padding-right: \").concat(gap, \"px \").concat(important, \";\\n }\\n body[\").concat(lockAttribute, \"] {\\n overflow: hidden \").concat(important, \";\\n overscroll-behavior: contain;\\n \").concat([\n allowRelative && \"position: relative \".concat(important, \";\"),\n gapMode === 'margin' &&\n \"\\n padding-left: \".concat(left, \"px;\\n padding-top: \").concat(top, \"px;\\n padding-right: \").concat(right, \"px;\\n margin-left:0;\\n margin-top:0;\\n margin-right: \").concat(gap, \"px \").concat(important, \";\\n \"),\n gapMode === 'padding' && \"padding-right: \".concat(gap, \"px \").concat(important, \";\"),\n ]\n .filter(Boolean)\n .join(''), \"\\n }\\n \\n .\").concat(zeroRightClassName, \" {\\n right: \").concat(gap, \"px \").concat(important, \";\\n }\\n \\n .\").concat(fullWidthClassName, \" {\\n margin-right: \").concat(gap, \"px \").concat(important, \";\\n }\\n \\n .\").concat(zeroRightClassName, \" .\").concat(zeroRightClassName, \" {\\n right: 0 \").concat(important, \";\\n }\\n \\n .\").concat(fullWidthClassName, \" .\").concat(fullWidthClassName, \" {\\n margin-right: 0 \").concat(important, \";\\n }\\n \\n body[\").concat(lockAttribute, \"] {\\n \").concat(removedBarSizeVariable, \": \").concat(gap, \"px;\\n }\\n\");\n};\nvar getCurrentUseCounter = function () {\n var counter = parseInt(document.body.getAttribute(lockAttribute) || '0', 10);\n return isFinite(counter) ? counter : 0;\n};\nexport var useLockAttribute = function () {\n React.useEffect(function () {\n document.body.setAttribute(lockAttribute, (getCurrentUseCounter() + 1).toString());\n return function () {\n var newCounter = getCurrentUseCounter() - 1;\n if (newCounter <= 0) {\n document.body.removeAttribute(lockAttribute);\n }\n else {\n document.body.setAttribute(lockAttribute, newCounter.toString());\n }\n };\n }, []);\n};\n/**\n * Removes page scrollbar and blocks page scroll when mounted\n */\nexport var RemoveScrollBar = function (_a) {\n var noRelative = _a.noRelative, noImportant = _a.noImportant, _b = _a.gapMode, gapMode = _b === void 0 ? 'margin' : _b;\n useLockAttribute();\n /*\n gap will be measured on every component mount\n however it will be used only by the \"first\" invocation\n due to singleton nature of Math.abs(deltaY) ? 'h' : 'v';\n // allow horizontal touch move on Range inputs. They will not cause any scroll\n if ('touches' in event && moveDirection === 'h' && target.type === 'range') {\n return false;\n }\n // allow drag selection (iOS); check if selection's anchorNode is the same as target or contains target\n var selection = window.getSelection();\n var anchorNode = selection && selection.anchorNode;\n var isTouchingSelection = anchorNode ? anchorNode === target || anchorNode.contains(target) : false;\n if (isTouchingSelection) {\n return false;\n }\n var canBeScrolledInMainDirection = locationCouldBeScrolled(moveDirection, target);\n if (!canBeScrolledInMainDirection) {\n return true;\n }\n if (canBeScrolledInMainDirection) {\n currentAxis = moveDirection;\n }\n else {\n currentAxis = moveDirection === 'v' ? 'h' : 'v';\n canBeScrolledInMainDirection = locationCouldBeScrolled(moveDirection, target);\n // other axis might be not scrollable\n }\n if (!canBeScrolledInMainDirection) {\n return false;\n }\n if (!activeAxis.current && 'changedTouches' in event && (deltaX || deltaY)) {\n activeAxis.current = currentAxis;\n }\n if (!currentAxis) {\n return true;\n }\n var cancelingAxis = activeAxis.current || currentAxis;\n return handleScroll(cancelingAxis, parent, event, cancelingAxis === 'h' ? deltaX : deltaY, true);\n }, []);\n var shouldPrevent = React.useCallback(function (_event) {\n var event = _event;\n if (!lockStack.length || lockStack[lockStack.length - 1] !== Style) {\n // not the last active\n return;\n }\n var delta = 'deltaY' in event ? getDeltaXY(event) : getTouchXY(event);\n var sourceEvent = shouldPreventQueue.current.filter(function (e) { return e.name === event.type && (e.target === event.target || event.target === e.shadowParent) && deltaCompare(e.delta, delta); })[0];\n // self event, and should be canceled\n if (sourceEvent && sourceEvent.should) {\n if (event.cancelable) {\n event.preventDefault();\n }\n return;\n }\n // outside or shard event\n if (!sourceEvent) {\n var shardNodes = (lastProps.current.shards || [])\n .map(extractRef)\n .filter(Boolean)\n .filter(function (node) { return node.contains(event.target); });\n var shouldStop = shardNodes.length > 0 ? shouldCancelEvent(event, shardNodes[0]) : !lastProps.current.noIsolation;\n if (shouldStop) {\n if (event.cancelable) {\n event.preventDefault();\n }\n }\n }\n }, []);\n var shouldCancel = React.useCallback(function (name, delta, target, should) {\n var event = { name: name, delta: delta, target: target, should: should, shadowParent: getOutermostShadowParent(target) };\n shouldPreventQueue.current.push(event);\n setTimeout(function () {\n shouldPreventQueue.current = shouldPreventQueue.current.filter(function (e) { return e !== event; });\n }, 1);\n }, []);\n var scrollTouchStart = React.useCallback(function (event) {\n touchStartRef.current = getTouchXY(event);\n activeAxis.current = undefined;\n }, []);\n var scrollWheel = React.useCallback(function (event) {\n shouldCancel(event.type, getDeltaXY(event), event.target, shouldCancelEvent(event, props.lockRef.current));\n }, []);\n var scrollTouchMove = React.useCallback(function (event) {\n shouldCancel(event.type, getTouchXY(event), event.target, shouldCancelEvent(event, props.lockRef.current));\n }, []);\n React.useEffect(function () {\n lockStack.push(Style);\n props.setCallbacks({\n onScrollCapture: scrollWheel,\n onWheelCapture: scrollWheel,\n onTouchMoveCapture: scrollTouchMove,\n });\n document.addEventListener('wheel', shouldPrevent, nonPassive);\n document.addEventListener('touchmove', shouldPrevent, nonPassive);\n document.addEventListener('touchstart', scrollTouchStart, nonPassive);\n return function () {\n lockStack = lockStack.filter(function (inst) { return inst !== Style; });\n document.removeEventListener('wheel', shouldPrevent, nonPassive);\n document.removeEventListener('touchmove', shouldPrevent, nonPassive);\n document.removeEventListener('touchstart', scrollTouchStart, nonPassive);\n };\n }, []);\n var removeScrollBar = props.removeScrollBar, inert = props.inert;\n return (React.createElement(React.Fragment, null,\n inert ? React.createElement(Style, { styles: generateStyle(id) }) : null,\n removeScrollBar ? React.createElement(RemoveScrollBar, { noRelative: props.noRelative, gapMode: props.gapMode }) : null));\n}\nfunction getOutermostShadowParent(node) {\n var shadowParent = null;\n while (node !== null) {\n if (node instanceof ShadowRoot) {\n shadowParent = node.host;\n node = node.host;\n }\n node = node.parentNode;\n }\n return shadowParent;\n}\n","import { __assign, __rest } from \"tslib\";\nimport * as React from 'react';\nimport { fullWidthClassName, zeroRightClassName } from 'react-remove-scroll-bar/constants';\nimport { useMergeRefs } from 'use-callback-ref';\nimport { effectCar } from './medium';\nvar nothing = function () {\n return;\n};\n/**\n * Removes scrollbar from the page and contain the scroll within the Lock\n */\nvar RemoveScroll = React.forwardRef(function (props, parentRef) {\n var ref = React.useRef(null);\n var _a = React.useState({\n onScrollCapture: nothing,\n onWheelCapture: nothing,\n onTouchMoveCapture: nothing,\n }), callbacks = _a[0], setCallbacks = _a[1];\n var forwardProps = props.forwardProps, children = props.children, className = props.className, removeScrollBar = props.removeScrollBar, enabled = props.enabled, shards = props.shards, sideCar = props.sideCar, noRelative = props.noRelative, noIsolation = props.noIsolation, inert = props.inert, allowPinchZoom = props.allowPinchZoom, _b = props.as, Container = _b === void 0 ? 'div' : _b, gapMode = props.gapMode, rest = __rest(props, [\"forwardProps\", \"children\", \"className\", \"removeScrollBar\", \"enabled\", \"shards\", \"sideCar\", \"noRelative\", \"noIsolation\", \"inert\", \"allowPinchZoom\", \"as\", \"gapMode\"]);\n var SideCar = sideCar;\n var containerRef = useMergeRefs([ref, parentRef]);\n var containerProps = __assign(__assign({}, rest), callbacks);\n return (React.createElement(React.Fragment, null,\n enabled && (React.createElement(SideCar, { sideCar: effectCar, removeScrollBar: removeScrollBar, shards: shards, noRelative: noRelative, noIsolation: noIsolation, inert: inert, setCallbacks: setCallbacks, allowPinchZoom: !!allowPinchZoom, lockRef: ref, gapMode: gapMode })),\n forwardProps ? (React.cloneElement(React.Children.only(children), __assign(__assign({}, containerProps), { ref: containerRef }))) : (React.createElement(Container, __assign({}, containerProps, { className: className, ref: containerRef }), children))));\n});\nRemoveScroll.defaultProps = {\n enabled: true,\n removeScrollBar: true,\n inert: false,\n};\nRemoveScroll.classNames = {\n fullWidth: fullWidthClassName,\n zeroRight: zeroRightClassName,\n};\nexport { RemoveScroll };\n","var passiveSupported = false;\nif (typeof window !== 'undefined') {\n try {\n var options = Object.defineProperty({}, 'passive', {\n get: function () {\n passiveSupported = true;\n return true;\n },\n });\n // @ts-ignore\n window.addEventListener('test', options, options);\n // @ts-ignore\n window.removeEventListener('test', options, options);\n }\n catch (err) {\n passiveSupported = false;\n }\n}\nexport var nonPassive = passiveSupported ? { passive: false } : false;\n","var alwaysContainsScroll = function (node) {\n // textarea will always _contain_ scroll inside self. It only can be hidden\n return node.tagName === 'TEXTAREA';\n};\nvar elementCanBeScrolled = function (node, overflow) {\n if (!(node instanceof Element)) {\n return false;\n }\n var styles = window.getComputedStyle(node);\n return (\n // not-not-scrollable\n styles[overflow] !== 'hidden' &&\n // contains scroll inside self\n !(styles.overflowY === styles.overflowX && !alwaysContainsScroll(node) && styles[overflow] === 'visible'));\n};\nvar elementCouldBeVScrolled = function (node) { return elementCanBeScrolled(node, 'overflowY'); };\nvar elementCouldBeHScrolled = function (node) { return elementCanBeScrolled(node, 'overflowX'); };\nexport var locationCouldBeScrolled = function (axis, node) {\n var ownerDocument = node.ownerDocument;\n var current = node;\n do {\n // Skip over shadow root\n if (typeof ShadowRoot !== 'undefined' && current instanceof ShadowRoot) {\n current = current.host;\n }\n var isScrollable = elementCouldBeScrolled(axis, current);\n if (isScrollable) {\n var _a = getScrollVariables(axis, current), scrollHeight = _a[1], clientHeight = _a[2];\n if (scrollHeight > clientHeight) {\n return true;\n }\n }\n current = current.parentNode;\n } while (current && current !== ownerDocument.body);\n return false;\n};\nvar getVScrollVariables = function (_a) {\n var scrollTop = _a.scrollTop, scrollHeight = _a.scrollHeight, clientHeight = _a.clientHeight;\n return [\n scrollTop,\n scrollHeight,\n clientHeight,\n ];\n};\nvar getHScrollVariables = function (_a) {\n var scrollLeft = _a.scrollLeft, scrollWidth = _a.scrollWidth, clientWidth = _a.clientWidth;\n return [\n scrollLeft,\n scrollWidth,\n clientWidth,\n ];\n};\nvar elementCouldBeScrolled = function (axis, node) {\n return axis === 'v' ? elementCouldBeVScrolled(node) : elementCouldBeHScrolled(node);\n};\nvar getScrollVariables = function (axis, node) {\n return axis === 'v' ? getVScrollVariables(node) : getHScrollVariables(node);\n};\nvar getDirectionFactor = function (axis, direction) {\n /**\n * If the element's direction is rtl (right-to-left), then scrollLeft is 0 when the scrollbar is at its rightmost position,\n * and then increasingly negative as you scroll towards the end of the content.\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft\n */\n return axis === 'h' && direction === 'rtl' ? -1 : 1;\n};\nexport var handleScroll = function (axis, endTarget, event, sourceDelta, noOverscroll) {\n var directionFactor = getDirectionFactor(axis, window.getComputedStyle(endTarget).direction);\n var delta = directionFactor * sourceDelta;\n // find scrollable target\n var target = event.target;\n var targetInLock = endTarget.contains(target);\n var shouldCancelScroll = false;\n var isDeltaPositive = delta > 0;\n var availableScroll = 0;\n var availableScrollTop = 0;\n do {\n if (!target) {\n break;\n }\n var _a = getScrollVariables(axis, target), position = _a[0], scroll_1 = _a[1], capacity = _a[2];\n var elementScroll = scroll_1 - capacity - directionFactor * position;\n if (position || elementScroll) {\n if (elementCouldBeScrolled(axis, target)) {\n availableScroll += elementScroll;\n availableScrollTop += position;\n }\n }\n var parent_1 = target.parentNode;\n // we will \"bubble\" from ShadowDom in case we are, or just to the parent in normal case\n // this is the same logic used in focus-lock\n target = (parent_1 && parent_1.nodeType === Node.DOCUMENT_FRAGMENT_NODE ? parent_1.host : parent_1);\n } while (\n // portaled content\n (!targetInLock && target !== document.body) ||\n // self content\n (targetInLock && (endTarget.contains(target) || endTarget === target)));\n // handle epsilon around 0 (non standard zoom levels)\n if (isDeltaPositive &&\n ((noOverscroll && Math.abs(availableScroll) < 1) || (!noOverscroll && delta > availableScroll))) {\n shouldCancelScroll = true;\n }\n else if (!isDeltaPositive &&\n ((noOverscroll && Math.abs(availableScrollTop) < 1) || (!noOverscroll && -delta > availableScrollTop))) {\n shouldCancelScroll = true;\n }\n return shouldCancelScroll;\n};\n","import { createSidecarMedium } from 'use-sidecar';\nexport var effectCar = createSidecarMedium();\n","import { exportSidecar } from 'use-sidecar';\nimport { RemoveScrollSideCar } from './SideEffect';\nimport { effectCar } from './medium';\nexport default exportSidecar(effectCar, RemoveScrollSideCar);\n","import { styleHookSingleton } from './hook';\n/**\n * create a Component to add styles on demand\n * - styles are added when first instance is mounted\n * - styles are removed when the last instance is unmounted\n * - changing styles in runtime does nothing unless dynamic is set. But with multiple components that can lead to the undefined behavior\n */\nexport var styleSingleton = function () {\n var useStyle = styleHookSingleton();\n var Sheet = function (_a) {\n var styles = _a.styles, dynamic = _a.dynamic;\n useStyle(styles, dynamic);\n return null;\n };\n return Sheet;\n};\n","import * as React from 'react';\nimport { stylesheetSingleton } from './singleton';\n/**\n * creates a hook to control style singleton\n * @see {@link styleSingleton} for a safer component version\n * @example\n * ```tsx\n * const useStyle = styleHookSingleton();\n * ///\n * useStyle('body { overflow: hidden}');\n */\nexport var styleHookSingleton = function () {\n var sheet = stylesheetSingleton();\n return function (styles, isDynamic) {\n React.useEffect(function () {\n sheet.add(styles);\n return function () {\n sheet.remove();\n };\n }, [styles && isDynamic]);\n };\n};\n","export { styleSingleton } from './component';\nexport { stylesheetSingleton } from './singleton';\nexport { styleHookSingleton } from './hook';\n","import { getNonce } from 'get-nonce';\nfunction makeStyleTag() {\n if (!document)\n return null;\n var tag = document.createElement('style');\n tag.type = 'text/css';\n var nonce = getNonce();\n if (nonce) {\n tag.setAttribute('nonce', nonce);\n }\n return tag;\n}\nfunction injectStyles(tag, css) {\n // @ts-ignore\n if (tag.styleSheet) {\n // @ts-ignore\n tag.styleSheet.cssText = css;\n }\n else {\n tag.appendChild(document.createTextNode(css));\n }\n}\nfunction insertStyleTag(tag) {\n var head = document.head || document.getElementsByTagName('head')[0];\n head.appendChild(tag);\n}\nexport var stylesheetSingleton = function () {\n var counter = 0;\n var stylesheet = null;\n return {\n add: function (style) {\n if (counter == 0) {\n if ((stylesheet = makeStyleTag())) {\n injectStyles(stylesheet, style);\n insertStyleTag(stylesheet);\n }\n }\n counter++;\n },\n remove: function () {\n counter--;\n if (!counter && stylesheet) {\n stylesheet.parentNode && stylesheet.parentNode.removeChild(stylesheet);\n stylesheet = null;\n }\n },\n };\n};\n","/**\n * Assigns a value for a given ref, no matter of the ref format\n * @param {RefObject} ref - a callback function or ref object\n * @param value - a new value\n *\n * @see https://github.com/theKashey/use-callback-ref#assignref\n * @example\n * const refObject = useRef();\n * const refFn = (ref) => {....}\n *\n * assignRef(refObject, \"refValue\");\n * assignRef(refFn, \"refValue\");\n */\nexport function assignRef(ref, value) {\n if (typeof ref === 'function') {\n ref(value);\n }\n else if (ref) {\n ref.current = value;\n }\n return ref;\n}\n","import * as React from 'react';\nimport { assignRef } from './assignRef';\nimport { useCallbackRef } from './useRef';\nvar useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;\nvar currentValues = new WeakMap();\n/**\n * Merges two or more refs together providing a single interface to set their value\n * @param {RefObject|Ref} refs\n * @returns {MutableRefObject} - a new ref, which translates all changes to {refs}\n *\n * @see {@link mergeRefs} a version without buit-in memoization\n * @see https://github.com/theKashey/use-callback-ref#usemergerefs\n * @example\n * const Component = React.forwardRef((props, ref) => {\n * const ownRef = useRef();\n * const domRef = useMergeRefs([ref, ownRef]); // 👈 merge together\n * return
...
\n * }\n */\nexport function useMergeRefs(refs, defaultValue) {\n var callbackRef = useCallbackRef(defaultValue || null, function (newValue) {\n return refs.forEach(function (ref) { return assignRef(ref, newValue); });\n });\n // handle refs changes - added or removed\n useIsomorphicLayoutEffect(function () {\n var oldValue = currentValues.get(callbackRef);\n if (oldValue) {\n var prevRefs_1 = new Set(oldValue);\n var nextRefs_1 = new Set(refs);\n var current_1 = callbackRef.current;\n prevRefs_1.forEach(function (ref) {\n if (!nextRefs_1.has(ref)) {\n assignRef(ref, null);\n }\n });\n nextRefs_1.forEach(function (ref) {\n if (!prevRefs_1.has(ref)) {\n assignRef(ref, current_1);\n }\n });\n }\n currentValues.set(callbackRef, refs);\n }, [refs]);\n return callbackRef;\n}\n","import { useState } from 'react';\n/**\n * creates a MutableRef with ref change callback\n * @param initialValue - initial ref value\n * @param {Function} callback - a callback to run when value changes\n *\n * @example\n * const ref = useCallbackRef(0, (newValue, oldValue) => console.log(oldValue, '->', newValue);\n * ref.current = 1;\n * // prints 0 -> 1\n *\n * @see https://reactjs.org/docs/hooks-reference.html#useref\n * @see https://github.com/theKashey/use-callback-ref#usecallbackref---to-replace-reactuseref\n * @returns {MutableRefObject}\n */\nexport function useCallbackRef(initialValue, callback) {\n var ref = useState(function () { return ({\n // value\n value: initialValue,\n // last callback\n callback: callback,\n // \"memoized\" public interface\n facade: {\n get current() {\n return ref.value;\n },\n set current(value) {\n var last = ref.value;\n if (last !== value) {\n ref.value = value;\n ref.callback(value, last);\n }\n },\n },\n }); })[0];\n // update callback\n ref.callback = callback;\n return ref.facade;\n}\n","import { __assign, __rest } from \"tslib\";\nimport * as React from 'react';\nvar SideCar = function (_a) {\n var sideCar = _a.sideCar, rest = __rest(_a, [\"sideCar\"]);\n if (!sideCar) {\n throw new Error('Sidecar: please provide `sideCar` property to import the right car');\n }\n var Target = sideCar.read();\n if (!Target) {\n throw new Error('Sidecar medium not found');\n }\n return React.createElement(Target, __assign({}, rest));\n};\nSideCar.isSideCarExport = true;\nexport function exportSidecar(medium, exported) {\n medium.useMedium(exported);\n return SideCar;\n}\n","import { __assign } from \"tslib\";\nfunction ItoI(a) {\n return a;\n}\nfunction innerCreateMedium(defaults, middleware) {\n if (middleware === void 0) { middleware = ItoI; }\n var buffer = [];\n var assigned = false;\n var medium = {\n read: function () {\n if (assigned) {\n throw new Error('Sidecar: could not `read` from an `assigned` medium. `read` could be used only with `useMedium`.');\n }\n if (buffer.length) {\n return buffer[buffer.length - 1];\n }\n return defaults;\n },\n useMedium: function (data) {\n var item = middleware(data, assigned);\n buffer.push(item);\n return function () {\n buffer = buffer.filter(function (x) { return x !== item; });\n };\n },\n assignSyncMedium: function (cb) {\n assigned = true;\n while (buffer.length) {\n var cbs = buffer;\n buffer = [];\n cbs.forEach(cb);\n }\n buffer = {\n push: function (x) { return cb(x); },\n filter: function () { return buffer; },\n };\n },\n assignMedium: function (cb) {\n assigned = true;\n var pendingQueue = [];\n if (buffer.length) {\n var cbs = buffer;\n buffer = [];\n cbs.forEach(cb);\n pendingQueue = buffer;\n }\n var executeQueue = function () {\n var cbs = pendingQueue;\n pendingQueue = [];\n cbs.forEach(cb);\n };\n var cycle = function () { return Promise.resolve().then(executeQueue); };\n cycle();\n buffer = {\n push: function (x) {\n pendingQueue.push(x);\n cycle();\n },\n filter: function (filter) {\n pendingQueue = pendingQueue.filter(filter);\n return buffer;\n },\n };\n },\n };\n return medium;\n}\nexport function createMedium(defaults, middleware) {\n if (middleware === void 0) { middleware = ItoI; }\n return innerCreateMedium(defaults, middleware);\n}\n// eslint-disable-next-line @typescript-eslint/ban-types\nexport function createSidecarMedium(options) {\n if (options === void 0) { options = {}; }\n var medium = innerCreateMedium(null);\n medium.options = __assign({ async: true, ssr: false }, options);\n return medium;\n}\n","module.exports = window[\"React\"];","module.exports = window[\"ReactDOM\"];","module.exports = window[\"ReactJSXRuntime\"];","module.exports = window[\"wp\"][\"apiFetch\"];","module.exports = window[\"wp\"][\"components\"];","module.exports = window[\"wp\"][\"data\"];","module.exports = window[\"wp\"][\"element\"];","module.exports = window[\"wp\"][\"i18n\"];","import { getSideAxis, getAlignmentAxis, getAxisLength, getSide, getAlignment, evaluate, getPaddingObject, rectToClientRect, min, clamp, placements, getAlignmentSides, getOppositeAlignmentPlacement, getOppositePlacement, getExpandedPlacements, getOppositeAxisPlacements, sides, max, getOppositeAxis } from '@floating-ui/utils';\nexport { rectToClientRect } from '@floating-ui/utils';\n\nfunction computeCoordsFromPlacement(_ref, placement, rtl) {\n let {\n reference,\n floating\n } = _ref;\n const sideAxis = getSideAxis(placement);\n const alignmentAxis = getAlignmentAxis(placement);\n const alignLength = getAxisLength(alignmentAxis);\n const side = getSide(placement);\n const isVertical = sideAxis === 'y';\n const commonX = reference.x + reference.width / 2 - floating.width / 2;\n const commonY = reference.y + reference.height / 2 - floating.height / 2;\n const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2;\n let coords;\n switch (side) {\n case 'top':\n coords = {\n x: commonX,\n y: reference.y - floating.height\n };\n break;\n case 'bottom':\n coords = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n case 'right':\n coords = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n case 'left':\n coords = {\n x: reference.x - floating.width,\n y: commonY\n };\n break;\n default:\n coords = {\n x: reference.x,\n y: reference.y\n };\n }\n switch (getAlignment(placement)) {\n case 'start':\n coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1);\n break;\n case 'end':\n coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1);\n break;\n }\n return coords;\n}\n\n/**\n * Resolves with an object of overflow side offsets that determine how much the\n * element is overflowing a given clipping boundary on each side.\n * - positive = overflowing the boundary by that number of pixels\n * - negative = how many pixels left before it will overflow\n * - 0 = lies flush with the boundary\n * @see https://floating-ui.com/docs/detectOverflow\n */\nasync function detectOverflow(state, options) {\n var _await$platform$isEle;\n if (options === void 0) {\n options = {};\n }\n const {\n x,\n y,\n platform,\n rects,\n elements,\n strategy\n } = state;\n const {\n boundary = 'clippingAncestors',\n rootBoundary = 'viewport',\n elementContext = 'floating',\n altBoundary = false,\n padding = 0\n } = evaluate(options, state);\n const paddingObject = getPaddingObject(padding);\n const altContext = elementContext === 'floating' ? 'reference' : 'floating';\n const element = elements[altBoundary ? altContext : elementContext];\n const clippingClientRect = rectToClientRect(await platform.getClippingRect({\n element: ((_await$platform$isEle = await (platform.isElement == null ? void 0 : platform.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || (await (platform.getDocumentElement == null ? void 0 : platform.getDocumentElement(elements.floating))),\n boundary,\n rootBoundary,\n strategy\n }));\n const rect = elementContext === 'floating' ? {\n x,\n y,\n width: rects.floating.width,\n height: rects.floating.height\n } : rects.reference;\n const offsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(elements.floating));\n const offsetScale = (await (platform.isElement == null ? void 0 : platform.isElement(offsetParent))) ? (await (platform.getScale == null ? void 0 : platform.getScale(offsetParent))) || {\n x: 1,\n y: 1\n } : {\n x: 1,\n y: 1\n };\n const elementClientRect = rectToClientRect(platform.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform.convertOffsetParentRelativeRectToViewportRelativeRect({\n elements,\n rect,\n offsetParent,\n strategy\n }) : rect);\n return {\n top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y,\n bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y,\n left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x,\n right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x\n };\n}\n\n/**\n * Computes the `x` and `y` coordinates that will place the floating element\n * next to a given reference element.\n *\n * This export does not have any `platform` interface logic. You will need to\n * write one for the platform you are using Floating UI with.\n */\nconst computePosition = async (reference, floating, config) => {\n const {\n placement = 'bottom',\n strategy = 'absolute',\n middleware = [],\n platform\n } = config;\n const validMiddleware = middleware.filter(Boolean);\n const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(floating));\n let rects = await platform.getElementRects({\n reference,\n floating,\n strategy\n });\n let {\n x,\n y\n } = computeCoordsFromPlacement(rects, placement, rtl);\n let statefulPlacement = placement;\n let middlewareData = {};\n let resetCount = 0;\n for (let i = 0; i < validMiddleware.length; i++) {\n var _platform$detectOverf;\n const {\n name,\n fn\n } = validMiddleware[i];\n const {\n x: nextX,\n y: nextY,\n data,\n reset\n } = await fn({\n x,\n y,\n initialPlacement: placement,\n placement: statefulPlacement,\n strategy,\n middlewareData,\n rects,\n platform: {\n ...platform,\n detectOverflow: (_platform$detectOverf = platform.detectOverflow) != null ? _platform$detectOverf : detectOverflow\n },\n elements: {\n reference,\n floating\n }\n });\n x = nextX != null ? nextX : x;\n y = nextY != null ? nextY : y;\n middlewareData = {\n ...middlewareData,\n [name]: {\n ...middlewareData[name],\n ...data\n }\n };\n if (reset && resetCount <= 50) {\n resetCount++;\n if (typeof reset === 'object') {\n if (reset.placement) {\n statefulPlacement = reset.placement;\n }\n if (reset.rects) {\n rects = reset.rects === true ? await platform.getElementRects({\n reference,\n floating,\n strategy\n }) : reset.rects;\n }\n ({\n x,\n y\n } = computeCoordsFromPlacement(rects, statefulPlacement, rtl));\n }\n i = -1;\n }\n }\n return {\n x,\n y,\n placement: statefulPlacement,\n strategy,\n middlewareData\n };\n};\n\n/**\n * Provides data to position an inner element of the floating element so that it\n * appears centered to the reference element.\n * @see https://floating-ui.com/docs/arrow\n */\nconst arrow = options => ({\n name: 'arrow',\n options,\n async fn(state) {\n const {\n x,\n y,\n placement,\n rects,\n platform,\n elements,\n middlewareData\n } = state;\n // Since `element` is required, we don't Partial<> the type.\n const {\n element,\n padding = 0\n } = evaluate(options, state) || {};\n if (element == null) {\n return {};\n }\n const paddingObject = getPaddingObject(padding);\n const coords = {\n x,\n y\n };\n const axis = getAlignmentAxis(placement);\n const length = getAxisLength(axis);\n const arrowDimensions = await platform.getDimensions(element);\n const isYAxis = axis === 'y';\n const minProp = isYAxis ? 'top' : 'left';\n const maxProp = isYAxis ? 'bottom' : 'right';\n const clientProp = isYAxis ? 'clientHeight' : 'clientWidth';\n const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length];\n const startDiff = coords[axis] - rects.reference[axis];\n const arrowOffsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(element));\n let clientSize = arrowOffsetParent ? arrowOffsetParent[clientProp] : 0;\n\n // DOM platform can return `window` as the `offsetParent`.\n if (!clientSize || !(await (platform.isElement == null ? void 0 : platform.isElement(arrowOffsetParent)))) {\n clientSize = elements.floating[clientProp] || rects.floating[length];\n }\n const centerToReference = endDiff / 2 - startDiff / 2;\n\n // If the padding is large enough that it causes the arrow to no longer be\n // centered, modify the padding so that it is centered.\n const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1;\n const minPadding = min(paddingObject[minProp], largestPossiblePadding);\n const maxPadding = min(paddingObject[maxProp], largestPossiblePadding);\n\n // Make sure the arrow doesn't overflow the floating element if the center\n // point is outside the floating element's bounds.\n const min$1 = minPadding;\n const max = clientSize - arrowDimensions[length] - maxPadding;\n const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference;\n const offset = clamp(min$1, center, max);\n\n // If the reference is small enough that the arrow's padding causes it to\n // to point to nothing for an aligned placement, adjust the offset of the\n // floating element itself. To ensure `shift()` continues to take action,\n // a single reset is performed when this is true.\n const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset && rects.reference[length] / 2 - (center < min$1 ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0;\n const alignmentOffset = shouldAddOffset ? center < min$1 ? center - min$1 : center - max : 0;\n return {\n [axis]: coords[axis] + alignmentOffset,\n data: {\n [axis]: offset,\n centerOffset: center - offset - alignmentOffset,\n ...(shouldAddOffset && {\n alignmentOffset\n })\n },\n reset: shouldAddOffset\n };\n }\n});\n\nfunction getPlacementList(alignment, autoAlignment, allowedPlacements) {\n const allowedPlacementsSortedByAlignment = alignment ? [...allowedPlacements.filter(placement => getAlignment(placement) === alignment), ...allowedPlacements.filter(placement => getAlignment(placement) !== alignment)] : allowedPlacements.filter(placement => getSide(placement) === placement);\n return allowedPlacementsSortedByAlignment.filter(placement => {\n if (alignment) {\n return getAlignment(placement) === alignment || (autoAlignment ? getOppositeAlignmentPlacement(placement) !== placement : false);\n }\n return true;\n });\n}\n/**\n * Optimizes the visibility of the floating element by choosing the placement\n * that has the most space available automatically, without needing to specify a\n * preferred placement. Alternative to `flip`.\n * @see https://floating-ui.com/docs/autoPlacement\n */\nconst autoPlacement = function (options) {\n if (options === void 0) {\n options = {};\n }\n return {\n name: 'autoPlacement',\n options,\n async fn(state) {\n var _middlewareData$autoP, _middlewareData$autoP2, _placementsThatFitOnE;\n const {\n rects,\n middlewareData,\n placement,\n platform,\n elements\n } = state;\n const {\n crossAxis = false,\n alignment,\n allowedPlacements = placements,\n autoAlignment = true,\n ...detectOverflowOptions\n } = evaluate(options, state);\n const placements$1 = alignment !== undefined || allowedPlacements === placements ? getPlacementList(alignment || null, autoAlignment, allowedPlacements) : allowedPlacements;\n const overflow = await platform.detectOverflow(state, detectOverflowOptions);\n const currentIndex = ((_middlewareData$autoP = middlewareData.autoPlacement) == null ? void 0 : _middlewareData$autoP.index) || 0;\n const currentPlacement = placements$1[currentIndex];\n if (currentPlacement == null) {\n return {};\n }\n const alignmentSides = getAlignmentSides(currentPlacement, rects, await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)));\n\n // Make `computeCoords` start from the right place.\n if (placement !== currentPlacement) {\n return {\n reset: {\n placement: placements$1[0]\n }\n };\n }\n const currentOverflows = [overflow[getSide(currentPlacement)], overflow[alignmentSides[0]], overflow[alignmentSides[1]]];\n const allOverflows = [...(((_middlewareData$autoP2 = middlewareData.autoPlacement) == null ? void 0 : _middlewareData$autoP2.overflows) || []), {\n placement: currentPlacement,\n overflows: currentOverflows\n }];\n const nextPlacement = placements$1[currentIndex + 1];\n\n // There are more placements to check.\n if (nextPlacement) {\n return {\n data: {\n index: currentIndex + 1,\n overflows: allOverflows\n },\n reset: {\n placement: nextPlacement\n }\n };\n }\n const placementsSortedByMostSpace = allOverflows.map(d => {\n const alignment = getAlignment(d.placement);\n return [d.placement, alignment && crossAxis ?\n // Check along the mainAxis and main crossAxis side.\n d.overflows.slice(0, 2).reduce((acc, v) => acc + v, 0) :\n // Check only the mainAxis.\n d.overflows[0], d.overflows];\n }).sort((a, b) => a[1] - b[1]);\n const placementsThatFitOnEachSide = placementsSortedByMostSpace.filter(d => d[2].slice(0,\n // Aligned placements should not check their opposite crossAxis\n // side.\n getAlignment(d[0]) ? 2 : 3).every(v => v <= 0));\n const resetPlacement = ((_placementsThatFitOnE = placementsThatFitOnEachSide[0]) == null ? void 0 : _placementsThatFitOnE[0]) || placementsSortedByMostSpace[0][0];\n if (resetPlacement !== placement) {\n return {\n data: {\n index: currentIndex + 1,\n overflows: allOverflows\n },\n reset: {\n placement: resetPlacement\n }\n };\n }\n return {};\n }\n };\n};\n\n/**\n * Optimizes the visibility of the floating element by flipping the `placement`\n * in order to keep it in view when the preferred placement(s) will overflow the\n * clipping boundary. Alternative to `autoPlacement`.\n * @see https://floating-ui.com/docs/flip\n */\nconst flip = function (options) {\n if (options === void 0) {\n options = {};\n }\n return {\n name: 'flip',\n options,\n async fn(state) {\n var _middlewareData$arrow, _middlewareData$flip;\n const {\n placement,\n middlewareData,\n rects,\n initialPlacement,\n platform,\n elements\n } = state;\n const {\n mainAxis: checkMainAxis = true,\n crossAxis: checkCrossAxis = true,\n fallbackPlacements: specifiedFallbackPlacements,\n fallbackStrategy = 'bestFit',\n fallbackAxisSideDirection = 'none',\n flipAlignment = true,\n ...detectOverflowOptions\n } = evaluate(options, state);\n\n // If a reset by the arrow was caused due to an alignment offset being\n // added, we should skip any logic now since `flip()` has already done its\n // work.\n // https://github.com/floating-ui/floating-ui/issues/2549#issuecomment-1719601643\n if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) {\n return {};\n }\n const side = getSide(placement);\n const initialSideAxis = getSideAxis(initialPlacement);\n const isBasePlacement = getSide(initialPlacement) === initialPlacement;\n const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating));\n const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement));\n const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== 'none';\n if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) {\n fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl));\n }\n const placements = [initialPlacement, ...fallbackPlacements];\n const overflow = await platform.detectOverflow(state, detectOverflowOptions);\n const overflows = [];\n let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || [];\n if (checkMainAxis) {\n overflows.push(overflow[side]);\n }\n if (checkCrossAxis) {\n const sides = getAlignmentSides(placement, rects, rtl);\n overflows.push(overflow[sides[0]], overflow[sides[1]]);\n }\n overflowsData = [...overflowsData, {\n placement,\n overflows\n }];\n\n // One or more sides is overflowing.\n if (!overflows.every(side => side <= 0)) {\n var _middlewareData$flip2, _overflowsData$filter;\n const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1;\n const nextPlacement = placements[nextIndex];\n if (nextPlacement) {\n const ignoreCrossAxisOverflow = checkCrossAxis === 'alignment' ? initialSideAxis !== getSideAxis(nextPlacement) : false;\n if (!ignoreCrossAxisOverflow ||\n // We leave the current main axis only if every placement on that axis\n // overflows the main axis.\n overflowsData.every(d => getSideAxis(d.placement) === initialSideAxis ? d.overflows[0] > 0 : true)) {\n // Try next placement and re-run the lifecycle.\n return {\n data: {\n index: nextIndex,\n overflows: overflowsData\n },\n reset: {\n placement: nextPlacement\n }\n };\n }\n }\n\n // First, find the candidates that fit on the mainAxis side of overflow,\n // then find the placement that fits the best on the main crossAxis side.\n let resetPlacement = (_overflowsData$filter = overflowsData.filter(d => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement;\n\n // Otherwise fallback.\n if (!resetPlacement) {\n switch (fallbackStrategy) {\n case 'bestFit':\n {\n var _overflowsData$filter2;\n const placement = (_overflowsData$filter2 = overflowsData.filter(d => {\n if (hasFallbackAxisSideDirection) {\n const currentSideAxis = getSideAxis(d.placement);\n return currentSideAxis === initialSideAxis ||\n // Create a bias to the `y` side axis due to horizontal\n // reading directions favoring greater width.\n currentSideAxis === 'y';\n }\n return true;\n }).map(d => [d.placement, d.overflows.filter(overflow => overflow > 0).reduce((acc, overflow) => acc + overflow, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0];\n if (placement) {\n resetPlacement = placement;\n }\n break;\n }\n case 'initialPlacement':\n resetPlacement = initialPlacement;\n break;\n }\n }\n if (placement !== resetPlacement) {\n return {\n reset: {\n placement: resetPlacement\n }\n };\n }\n }\n return {};\n }\n };\n};\n\nfunction getSideOffsets(overflow, rect) {\n return {\n top: overflow.top - rect.height,\n right: overflow.right - rect.width,\n bottom: overflow.bottom - rect.height,\n left: overflow.left - rect.width\n };\n}\nfunction isAnySideFullyClipped(overflow) {\n return sides.some(side => overflow[side] >= 0);\n}\n/**\n * Provides data to hide the floating element in applicable situations, such as\n * when it is not in the same clipping context as the reference element.\n * @see https://floating-ui.com/docs/hide\n */\nconst hide = function (options) {\n if (options === void 0) {\n options = {};\n }\n return {\n name: 'hide',\n options,\n async fn(state) {\n const {\n rects,\n platform\n } = state;\n const {\n strategy = 'referenceHidden',\n ...detectOverflowOptions\n } = evaluate(options, state);\n switch (strategy) {\n case 'referenceHidden':\n {\n const overflow = await platform.detectOverflow(state, {\n ...detectOverflowOptions,\n elementContext: 'reference'\n });\n const offsets = getSideOffsets(overflow, rects.reference);\n return {\n data: {\n referenceHiddenOffsets: offsets,\n referenceHidden: isAnySideFullyClipped(offsets)\n }\n };\n }\n case 'escaped':\n {\n const overflow = await platform.detectOverflow(state, {\n ...detectOverflowOptions,\n altBoundary: true\n });\n const offsets = getSideOffsets(overflow, rects.floating);\n return {\n data: {\n escapedOffsets: offsets,\n escaped: isAnySideFullyClipped(offsets)\n }\n };\n }\n default:\n {\n return {};\n }\n }\n }\n };\n};\n\nfunction getBoundingRect(rects) {\n const minX = min(...rects.map(rect => rect.left));\n const minY = min(...rects.map(rect => rect.top));\n const maxX = max(...rects.map(rect => rect.right));\n const maxY = max(...rects.map(rect => rect.bottom));\n return {\n x: minX,\n y: minY,\n width: maxX - minX,\n height: maxY - minY\n };\n}\nfunction getRectsByLine(rects) {\n const sortedRects = rects.slice().sort((a, b) => a.y - b.y);\n const groups = [];\n let prevRect = null;\n for (let i = 0; i < sortedRects.length; i++) {\n const rect = sortedRects[i];\n if (!prevRect || rect.y - prevRect.y > prevRect.height / 2) {\n groups.push([rect]);\n } else {\n groups[groups.length - 1].push(rect);\n }\n prevRect = rect;\n }\n return groups.map(rect => rectToClientRect(getBoundingRect(rect)));\n}\n/**\n * Provides improved positioning for inline reference elements that can span\n * over multiple lines, such as hyperlinks or range selections.\n * @see https://floating-ui.com/docs/inline\n */\nconst inline = function (options) {\n if (options === void 0) {\n options = {};\n }\n return {\n name: 'inline',\n options,\n async fn(state) {\n const {\n placement,\n elements,\n rects,\n platform,\n strategy\n } = state;\n // A MouseEvent's client{X,Y} coords can be up to 2 pixels off a\n // ClientRect's bounds, despite the event listener being triggered. A\n // padding of 2 seems to handle this issue.\n const {\n padding = 2,\n x,\n y\n } = evaluate(options, state);\n const nativeClientRects = Array.from((await (platform.getClientRects == null ? void 0 : platform.getClientRects(elements.reference))) || []);\n const clientRects = getRectsByLine(nativeClientRects);\n const fallback = rectToClientRect(getBoundingRect(nativeClientRects));\n const paddingObject = getPaddingObject(padding);\n function getBoundingClientRect() {\n // There are two rects and they are disjoined.\n if (clientRects.length === 2 && clientRects[0].left > clientRects[1].right && x != null && y != null) {\n // Find the first rect in which the point is fully inside.\n return clientRects.find(rect => x > rect.left - paddingObject.left && x < rect.right + paddingObject.right && y > rect.top - paddingObject.top && y < rect.bottom + paddingObject.bottom) || fallback;\n }\n\n // There are 2 or more connected rects.\n if (clientRects.length >= 2) {\n if (getSideAxis(placement) === 'y') {\n const firstRect = clientRects[0];\n const lastRect = clientRects[clientRects.length - 1];\n const isTop = getSide(placement) === 'top';\n const top = firstRect.top;\n const bottom = lastRect.bottom;\n const left = isTop ? firstRect.left : lastRect.left;\n const right = isTop ? firstRect.right : lastRect.right;\n const width = right - left;\n const height = bottom - top;\n return {\n top,\n bottom,\n left,\n right,\n width,\n height,\n x: left,\n y: top\n };\n }\n const isLeftSide = getSide(placement) === 'left';\n const maxRight = max(...clientRects.map(rect => rect.right));\n const minLeft = min(...clientRects.map(rect => rect.left));\n const measureRects = clientRects.filter(rect => isLeftSide ? rect.left === minLeft : rect.right === maxRight);\n const top = measureRects[0].top;\n const bottom = measureRects[measureRects.length - 1].bottom;\n const left = minLeft;\n const right = maxRight;\n const width = right - left;\n const height = bottom - top;\n return {\n top,\n bottom,\n left,\n right,\n width,\n height,\n x: left,\n y: top\n };\n }\n return fallback;\n }\n const resetRects = await platform.getElementRects({\n reference: {\n getBoundingClientRect\n },\n floating: elements.floating,\n strategy\n });\n if (rects.reference.x !== resetRects.reference.x || rects.reference.y !== resetRects.reference.y || rects.reference.width !== resetRects.reference.width || rects.reference.height !== resetRects.reference.height) {\n return {\n reset: {\n rects: resetRects\n }\n };\n }\n return {};\n }\n };\n};\n\nconst originSides = /*#__PURE__*/new Set(['left', 'top']);\n\n// For type backwards-compatibility, the `OffsetOptions` type was also\n// Derivable.\n\nasync function convertValueToCoords(state, options) {\n const {\n placement,\n platform,\n elements\n } = state;\n const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating));\n const side = getSide(placement);\n const alignment = getAlignment(placement);\n const isVertical = getSideAxis(placement) === 'y';\n const mainAxisMulti = originSides.has(side) ? -1 : 1;\n const crossAxisMulti = rtl && isVertical ? -1 : 1;\n const rawValue = evaluate(options, state);\n\n // eslint-disable-next-line prefer-const\n let {\n mainAxis,\n crossAxis,\n alignmentAxis\n } = typeof rawValue === 'number' ? {\n mainAxis: rawValue,\n crossAxis: 0,\n alignmentAxis: null\n } : {\n mainAxis: rawValue.mainAxis || 0,\n crossAxis: rawValue.crossAxis || 0,\n alignmentAxis: rawValue.alignmentAxis\n };\n if (alignment && typeof alignmentAxis === 'number') {\n crossAxis = alignment === 'end' ? alignmentAxis * -1 : alignmentAxis;\n }\n return isVertical ? {\n x: crossAxis * crossAxisMulti,\n y: mainAxis * mainAxisMulti\n } : {\n x: mainAxis * mainAxisMulti,\n y: crossAxis * crossAxisMulti\n };\n}\n\n/**\n * Modifies the placement by translating the floating element along the\n * specified axes.\n * A number (shorthand for `mainAxis` or distance), or an axes configuration\n * object may be passed.\n * @see https://floating-ui.com/docs/offset\n */\nconst offset = function (options) {\n if (options === void 0) {\n options = 0;\n }\n return {\n name: 'offset',\n options,\n async fn(state) {\n var _middlewareData$offse, _middlewareData$arrow;\n const {\n x,\n y,\n placement,\n middlewareData\n } = state;\n const diffCoords = await convertValueToCoords(state, options);\n\n // If the placement is the same and the arrow caused an alignment offset\n // then we don't need to change the positioning coordinates.\n if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) {\n return {};\n }\n return {\n x: x + diffCoords.x,\n y: y + diffCoords.y,\n data: {\n ...diffCoords,\n placement\n }\n };\n }\n };\n};\n\n/**\n * Optimizes the visibility of the floating element by shifting it in order to\n * keep it in view when it will overflow the clipping boundary.\n * @see https://floating-ui.com/docs/shift\n */\nconst shift = function (options) {\n if (options === void 0) {\n options = {};\n }\n return {\n name: 'shift',\n options,\n async fn(state) {\n const {\n x,\n y,\n placement,\n platform\n } = state;\n const {\n mainAxis: checkMainAxis = true,\n crossAxis: checkCrossAxis = false,\n limiter = {\n fn: _ref => {\n let {\n x,\n y\n } = _ref;\n return {\n x,\n y\n };\n }\n },\n ...detectOverflowOptions\n } = evaluate(options, state);\n const coords = {\n x,\n y\n };\n const overflow = await platform.detectOverflow(state, detectOverflowOptions);\n const crossAxis = getSideAxis(getSide(placement));\n const mainAxis = getOppositeAxis(crossAxis);\n let mainAxisCoord = coords[mainAxis];\n let crossAxisCoord = coords[crossAxis];\n if (checkMainAxis) {\n const minSide = mainAxis === 'y' ? 'top' : 'left';\n const maxSide = mainAxis === 'y' ? 'bottom' : 'right';\n const min = mainAxisCoord + overflow[minSide];\n const max = mainAxisCoord - overflow[maxSide];\n mainAxisCoord = clamp(min, mainAxisCoord, max);\n }\n if (checkCrossAxis) {\n const minSide = crossAxis === 'y' ? 'top' : 'left';\n const maxSide = crossAxis === 'y' ? 'bottom' : 'right';\n const min = crossAxisCoord + overflow[minSide];\n const max = crossAxisCoord - overflow[maxSide];\n crossAxisCoord = clamp(min, crossAxisCoord, max);\n }\n const limitedCoords = limiter.fn({\n ...state,\n [mainAxis]: mainAxisCoord,\n [crossAxis]: crossAxisCoord\n });\n return {\n ...limitedCoords,\n data: {\n x: limitedCoords.x - x,\n y: limitedCoords.y - y,\n enabled: {\n [mainAxis]: checkMainAxis,\n [crossAxis]: checkCrossAxis\n }\n }\n };\n }\n };\n};\n/**\n * Built-in `limiter` that will stop `shift()` at a certain point.\n */\nconst limitShift = function (options) {\n if (options === void 0) {\n options = {};\n }\n return {\n options,\n fn(state) {\n const {\n x,\n y,\n placement,\n rects,\n middlewareData\n } = state;\n const {\n offset = 0,\n mainAxis: checkMainAxis = true,\n crossAxis: checkCrossAxis = true\n } = evaluate(options, state);\n const coords = {\n x,\n y\n };\n const crossAxis = getSideAxis(placement);\n const mainAxis = getOppositeAxis(crossAxis);\n let mainAxisCoord = coords[mainAxis];\n let crossAxisCoord = coords[crossAxis];\n const rawOffset = evaluate(offset, state);\n const computedOffset = typeof rawOffset === 'number' ? {\n mainAxis: rawOffset,\n crossAxis: 0\n } : {\n mainAxis: 0,\n crossAxis: 0,\n ...rawOffset\n };\n if (checkMainAxis) {\n const len = mainAxis === 'y' ? 'height' : 'width';\n const limitMin = rects.reference[mainAxis] - rects.floating[len] + computedOffset.mainAxis;\n const limitMax = rects.reference[mainAxis] + rects.reference[len] - computedOffset.mainAxis;\n if (mainAxisCoord < limitMin) {\n mainAxisCoord = limitMin;\n } else if (mainAxisCoord > limitMax) {\n mainAxisCoord = limitMax;\n }\n }\n if (checkCrossAxis) {\n var _middlewareData$offse, _middlewareData$offse2;\n const len = mainAxis === 'y' ? 'width' : 'height';\n const isOriginSide = originSides.has(getSide(placement));\n const limitMin = rects.reference[crossAxis] - rects.floating[len] + (isOriginSide ? ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse[crossAxis]) || 0 : 0) + (isOriginSide ? 0 : computedOffset.crossAxis);\n const limitMax = rects.reference[crossAxis] + rects.reference[len] + (isOriginSide ? 0 : ((_middlewareData$offse2 = middlewareData.offset) == null ? void 0 : _middlewareData$offse2[crossAxis]) || 0) - (isOriginSide ? computedOffset.crossAxis : 0);\n if (crossAxisCoord < limitMin) {\n crossAxisCoord = limitMin;\n } else if (crossAxisCoord > limitMax) {\n crossAxisCoord = limitMax;\n }\n }\n return {\n [mainAxis]: mainAxisCoord,\n [crossAxis]: crossAxisCoord\n };\n }\n };\n};\n\n/**\n * Provides data that allows you to change the size of the floating element —\n * for instance, prevent it from overflowing the clipping boundary or match the\n * width of the reference element.\n * @see https://floating-ui.com/docs/size\n */\nconst size = function (options) {\n if (options === void 0) {\n options = {};\n }\n return {\n name: 'size',\n options,\n async fn(state) {\n var _state$middlewareData, _state$middlewareData2;\n const {\n placement,\n rects,\n platform,\n elements\n } = state;\n const {\n apply = () => {},\n ...detectOverflowOptions\n } = evaluate(options, state);\n const overflow = await platform.detectOverflow(state, detectOverflowOptions);\n const side = getSide(placement);\n const alignment = getAlignment(placement);\n const isYAxis = getSideAxis(placement) === 'y';\n const {\n width,\n height\n } = rects.floating;\n let heightSide;\n let widthSide;\n if (side === 'top' || side === 'bottom') {\n heightSide = side;\n widthSide = alignment === ((await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating))) ? 'start' : 'end') ? 'left' : 'right';\n } else {\n widthSide = side;\n heightSide = alignment === 'end' ? 'top' : 'bottom';\n }\n const maximumClippingHeight = height - overflow.top - overflow.bottom;\n const maximumClippingWidth = width - overflow.left - overflow.right;\n const overflowAvailableHeight = min(height - overflow[heightSide], maximumClippingHeight);\n const overflowAvailableWidth = min(width - overflow[widthSide], maximumClippingWidth);\n const noShift = !state.middlewareData.shift;\n let availableHeight = overflowAvailableHeight;\n let availableWidth = overflowAvailableWidth;\n if ((_state$middlewareData = state.middlewareData.shift) != null && _state$middlewareData.enabled.x) {\n availableWidth = maximumClippingWidth;\n }\n if ((_state$middlewareData2 = state.middlewareData.shift) != null && _state$middlewareData2.enabled.y) {\n availableHeight = maximumClippingHeight;\n }\n if (noShift && !alignment) {\n const xMin = max(overflow.left, 0);\n const xMax = max(overflow.right, 0);\n const yMin = max(overflow.top, 0);\n const yMax = max(overflow.bottom, 0);\n if (isYAxis) {\n availableWidth = width - 2 * (xMin !== 0 || xMax !== 0 ? xMin + xMax : max(overflow.left, overflow.right));\n } else {\n availableHeight = height - 2 * (yMin !== 0 || yMax !== 0 ? yMin + yMax : max(overflow.top, overflow.bottom));\n }\n }\n await apply({\n ...state,\n availableWidth,\n availableHeight\n });\n const nextDimensions = await platform.getDimensions(elements.floating);\n if (width !== nextDimensions.width || height !== nextDimensions.height) {\n return {\n reset: {\n rects: true\n }\n };\n }\n return {};\n }\n };\n};\n\nexport { arrow, autoPlacement, computePosition, detectOverflow, flip, hide, inline, limitShift, offset, shift, size };\n","import { rectToClientRect, arrow as arrow$1, autoPlacement as autoPlacement$1, detectOverflow as detectOverflow$1, flip as flip$1, hide as hide$1, inline as inline$1, limitShift as limitShift$1, offset as offset$1, shift as shift$1, size as size$1, computePosition as computePosition$1 } from '@floating-ui/core';\nimport { round, createCoords, max, min, floor } from '@floating-ui/utils';\nimport { getComputedStyle as getComputedStyle$1, isHTMLElement, isElement, getWindow, isWebKit, getFrameElement, getNodeScroll, getDocumentElement, isTopLayer, getNodeName, isOverflowElement, getOverflowAncestors, getParentNode, isLastTraversableNode, isContainingBlock, isTableElement, getContainingBlock } from '@floating-ui/utils/dom';\nexport { getOverflowAncestors } from '@floating-ui/utils/dom';\n\nfunction getCssDimensions(element) {\n const css = getComputedStyle$1(element);\n // In testing environments, the `width` and `height` properties are empty\n // strings for SVG elements, returning NaN. Fallback to `0` in this case.\n let width = parseFloat(css.width) || 0;\n let height = parseFloat(css.height) || 0;\n const hasOffset = isHTMLElement(element);\n const offsetWidth = hasOffset ? element.offsetWidth : width;\n const offsetHeight = hasOffset ? element.offsetHeight : height;\n const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight;\n if (shouldFallback) {\n width = offsetWidth;\n height = offsetHeight;\n }\n return {\n width,\n height,\n $: shouldFallback\n };\n}\n\nfunction unwrapElement(element) {\n return !isElement(element) ? element.contextElement : element;\n}\n\nfunction getScale(element) {\n const domElement = unwrapElement(element);\n if (!isHTMLElement(domElement)) {\n return createCoords(1);\n }\n const rect = domElement.getBoundingClientRect();\n const {\n width,\n height,\n $\n } = getCssDimensions(domElement);\n let x = ($ ? round(rect.width) : rect.width) / width;\n let y = ($ ? round(rect.height) : rect.height) / height;\n\n // 0, NaN, or Infinity should always fallback to 1.\n\n if (!x || !Number.isFinite(x)) {\n x = 1;\n }\n if (!y || !Number.isFinite(y)) {\n y = 1;\n }\n return {\n x,\n y\n };\n}\n\nconst noOffsets = /*#__PURE__*/createCoords(0);\nfunction getVisualOffsets(element) {\n const win = getWindow(element);\n if (!isWebKit() || !win.visualViewport) {\n return noOffsets;\n }\n return {\n x: win.visualViewport.offsetLeft,\n y: win.visualViewport.offsetTop\n };\n}\nfunction shouldAddVisualOffsets(element, isFixed, floatingOffsetParent) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow(element)) {\n return false;\n }\n return isFixed;\n}\n\nfunction getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n const clientRect = element.getBoundingClientRect();\n const domElement = unwrapElement(element);\n let scale = createCoords(1);\n if (includeScale) {\n if (offsetParent) {\n if (isElement(offsetParent)) {\n scale = getScale(offsetParent);\n }\n } else {\n scale = getScale(element);\n }\n }\n const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0);\n let x = (clientRect.left + visualOffsets.x) / scale.x;\n let y = (clientRect.top + visualOffsets.y) / scale.y;\n let width = clientRect.width / scale.x;\n let height = clientRect.height / scale.y;\n if (domElement) {\n const win = getWindow(domElement);\n const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent;\n let currentWin = win;\n let currentIFrame = getFrameElement(currentWin);\n while (currentIFrame && offsetParent && offsetWin !== currentWin) {\n const iframeScale = getScale(currentIFrame);\n const iframeRect = currentIFrame.getBoundingClientRect();\n const css = getComputedStyle$1(currentIFrame);\n const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x;\n const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y;\n x *= iframeScale.x;\n y *= iframeScale.y;\n width *= iframeScale.x;\n height *= iframeScale.y;\n x += left;\n y += top;\n currentWin = getWindow(currentIFrame);\n currentIFrame = getFrameElement(currentWin);\n }\n }\n return rectToClientRect({\n width,\n height,\n x,\n y\n });\n}\n\n// If has a CSS width greater than the viewport, then this will be\n// incorrect for RTL.\nfunction getWindowScrollBarX(element, rect) {\n const leftScroll = getNodeScroll(element).scrollLeft;\n if (!rect) {\n return getBoundingClientRect(getDocumentElement(element)).left + leftScroll;\n }\n return rect.left + leftScroll;\n}\n\nfunction getHTMLOffset(documentElement, scroll) {\n const htmlRect = documentElement.getBoundingClientRect();\n const x = htmlRect.left + scroll.scrollLeft - getWindowScrollBarX(documentElement, htmlRect);\n const y = htmlRect.top + scroll.scrollTop;\n return {\n x,\n y\n };\n}\n\nfunction convertOffsetParentRelativeRectToViewportRelativeRect(_ref) {\n let {\n elements,\n rect,\n offsetParent,\n strategy\n } = _ref;\n const isFixed = strategy === 'fixed';\n const documentElement = getDocumentElement(offsetParent);\n const topLayer = elements ? isTopLayer(elements.floating) : false;\n if (offsetParent === documentElement || topLayer && isFixed) {\n return rect;\n }\n let scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n let scale = createCoords(1);\n const offsets = createCoords(0);\n const isOffsetParentAnElement = isHTMLElement(offsetParent);\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n if (isHTMLElement(offsetParent)) {\n const offsetRect = getBoundingClientRect(offsetParent);\n scale = getScale(offsetParent);\n offsets.x = offsetRect.x + offsetParent.clientLeft;\n offsets.y = offsetRect.y + offsetParent.clientTop;\n }\n }\n const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0);\n return {\n width: rect.width * scale.x,\n height: rect.height * scale.y,\n x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x + htmlOffset.x,\n y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y + htmlOffset.y\n };\n}\n\nfunction getClientRects(element) {\n return Array.from(element.getClientRects());\n}\n\n// Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable.\nfunction getDocumentRect(element) {\n const html = getDocumentElement(element);\n const scroll = getNodeScroll(element);\n const body = element.ownerDocument.body;\n const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth);\n const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight);\n let x = -scroll.scrollLeft + getWindowScrollBarX(element);\n const y = -scroll.scrollTop;\n if (getComputedStyle$1(body).direction === 'rtl') {\n x += max(html.clientWidth, body.clientWidth) - width;\n }\n return {\n width,\n height,\n x,\n y\n };\n}\n\n// Safety check: ensure the scrollbar space is reasonable in case this\n// calculation is affected by unusual styles.\n// Most scrollbars leave 15-18px of space.\nconst SCROLLBAR_MAX = 25;\nfunction getViewportRect(element, strategy) {\n const win = getWindow(element);\n const html = getDocumentElement(element);\n const visualViewport = win.visualViewport;\n let width = html.clientWidth;\n let height = html.clientHeight;\n let x = 0;\n let y = 0;\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n const visualViewportBased = isWebKit();\n if (!visualViewportBased || visualViewportBased && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n const windowScrollbarX = getWindowScrollBarX(html);\n // `overflow: hidden` + `scrollbar-gutter: stable` reduces the\n // visual width of the but this is not considered in the size\n // of `html.clientWidth`.\n if (windowScrollbarX <= 0) {\n const doc = html.ownerDocument;\n const body = doc.body;\n const bodyStyles = getComputedStyle(body);\n const bodyMarginInline = doc.compatMode === 'CSS1Compat' ? parseFloat(bodyStyles.marginLeft) + parseFloat(bodyStyles.marginRight) || 0 : 0;\n const clippingStableScrollbarWidth = Math.abs(html.clientWidth - body.clientWidth - bodyMarginInline);\n if (clippingStableScrollbarWidth <= SCROLLBAR_MAX) {\n width -= clippingStableScrollbarWidth;\n }\n } else if (windowScrollbarX <= SCROLLBAR_MAX) {\n // If the scrollbar is on the left, the width needs to be extended\n // by the scrollbar amount so there isn't extra space on the right.\n width += windowScrollbarX;\n }\n return {\n width,\n height,\n x,\n y\n };\n}\n\nconst absoluteOrFixed = /*#__PURE__*/new Set(['absolute', 'fixed']);\n// Returns the inner client rect, subtracting scrollbars if present.\nfunction getInnerBoundingClientRect(element, strategy) {\n const clientRect = getBoundingClientRect(element, true, strategy === 'fixed');\n const top = clientRect.top + element.clientTop;\n const left = clientRect.left + element.clientLeft;\n const scale = isHTMLElement(element) ? getScale(element) : createCoords(1);\n const width = element.clientWidth * scale.x;\n const height = element.clientHeight * scale.y;\n const x = left * scale.x;\n const y = top * scale.y;\n return {\n width,\n height,\n x,\n y\n };\n}\nfunction getClientRectFromClippingAncestor(element, clippingAncestor, strategy) {\n let rect;\n if (clippingAncestor === 'viewport') {\n rect = getViewportRect(element, strategy);\n } else if (clippingAncestor === 'document') {\n rect = getDocumentRect(getDocumentElement(element));\n } else if (isElement(clippingAncestor)) {\n rect = getInnerBoundingClientRect(clippingAncestor, strategy);\n } else {\n const visualOffsets = getVisualOffsets(element);\n rect = {\n x: clippingAncestor.x - visualOffsets.x,\n y: clippingAncestor.y - visualOffsets.y,\n width: clippingAncestor.width,\n height: clippingAncestor.height\n };\n }\n return rectToClientRect(rect);\n}\nfunction hasFixedPositionAncestor(element, stopNode) {\n const parentNode = getParentNode(element);\n if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) {\n return false;\n }\n return getComputedStyle$1(parentNode).position === 'fixed' || hasFixedPositionAncestor(parentNode, stopNode);\n}\n\n// A \"clipping ancestor\" is an `overflow` element with the characteristic of\n// clipping (or hiding) child elements. This returns all clipping ancestors\n// of the given element up the tree.\nfunction getClippingElementAncestors(element, cache) {\n const cachedResult = cache.get(element);\n if (cachedResult) {\n return cachedResult;\n }\n let result = getOverflowAncestors(element, [], false).filter(el => isElement(el) && getNodeName(el) !== 'body');\n let currentContainingBlockComputedStyle = null;\n const elementIsFixed = getComputedStyle$1(element).position === 'fixed';\n let currentNode = elementIsFixed ? getParentNode(element) : element;\n\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n while (isElement(currentNode) && !isLastTraversableNode(currentNode)) {\n const computedStyle = getComputedStyle$1(currentNode);\n const currentNodeIsContaining = isContainingBlock(currentNode);\n if (!currentNodeIsContaining && computedStyle.position === 'fixed') {\n currentContainingBlockComputedStyle = null;\n }\n const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === 'static' && !!currentContainingBlockComputedStyle && absoluteOrFixed.has(currentContainingBlockComputedStyle.position) || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode);\n if (shouldDropCurrentNode) {\n // Drop non-containing blocks.\n result = result.filter(ancestor => ancestor !== currentNode);\n } else {\n // Record last containing block for next iteration.\n currentContainingBlockComputedStyle = computedStyle;\n }\n currentNode = getParentNode(currentNode);\n }\n cache.set(element, result);\n return result;\n}\n\n// Gets the maximum area that the element is visible in due to any number of\n// clipping ancestors.\nfunction getClippingRect(_ref) {\n let {\n element,\n boundary,\n rootBoundary,\n strategy\n } = _ref;\n const elementClippingAncestors = boundary === 'clippingAncestors' ? isTopLayer(element) ? [] : getClippingElementAncestors(element, this._c) : [].concat(boundary);\n const clippingAncestors = [...elementClippingAncestors, rootBoundary];\n const firstClippingAncestor = clippingAncestors[0];\n const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => {\n const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy));\n return {\n width: clippingRect.right - clippingRect.left,\n height: clippingRect.bottom - clippingRect.top,\n x: clippingRect.left,\n y: clippingRect.top\n };\n}\n\nfunction getDimensions(element) {\n const {\n width,\n height\n } = getCssDimensions(element);\n return {\n width,\n height\n };\n}\n\nfunction getRectRelativeToOffsetParent(element, offsetParent, strategy) {\n const isOffsetParentAnElement = isHTMLElement(offsetParent);\n const documentElement = getDocumentElement(offsetParent);\n const isFixed = strategy === 'fixed';\n const rect = getBoundingClientRect(element, true, isFixed, offsetParent);\n let scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n const offsets = createCoords(0);\n\n // If the scrollbar appears on the left (e.g. RTL systems). Use\n // Firefox with layout.scrollbar.side = 3 in about:config to test this.\n function setLeftRTLScrollbarOffset() {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n if (isOffsetParentAnElement) {\n const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent);\n offsets.x = offsetRect.x + offsetParent.clientLeft;\n offsets.y = offsetRect.y + offsetParent.clientTop;\n } else if (documentElement) {\n setLeftRTLScrollbarOffset();\n }\n }\n if (isFixed && !isOffsetParentAnElement && documentElement) {\n setLeftRTLScrollbarOffset();\n }\n const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0);\n const x = rect.left + scroll.scrollLeft - offsets.x - htmlOffset.x;\n const y = rect.top + scroll.scrollTop - offsets.y - htmlOffset.y;\n return {\n x,\n y,\n width: rect.width,\n height: rect.height\n };\n}\n\nfunction isStaticPositioned(element) {\n return getComputedStyle$1(element).position === 'static';\n}\n\nfunction getTrueOffsetParent(element, polyfill) {\n if (!isHTMLElement(element) || getComputedStyle$1(element).position === 'fixed') {\n return null;\n }\n if (polyfill) {\n return polyfill(element);\n }\n let rawOffsetParent = element.offsetParent;\n\n // Firefox returns the element as the offsetParent if it's non-static,\n // while Chrome and Safari return the element. The element must\n // be used to perform the correct calculations even if the element is\n // non-static.\n if (getDocumentElement(element) === rawOffsetParent) {\n rawOffsetParent = rawOffsetParent.ownerDocument.body;\n }\n return rawOffsetParent;\n}\n\n// Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\nfunction getOffsetParent(element, polyfill) {\n const win = getWindow(element);\n if (isTopLayer(element)) {\n return win;\n }\n if (!isHTMLElement(element)) {\n let svgOffsetParent = getParentNode(element);\n while (svgOffsetParent && !isLastTraversableNode(svgOffsetParent)) {\n if (isElement(svgOffsetParent) && !isStaticPositioned(svgOffsetParent)) {\n return svgOffsetParent;\n }\n svgOffsetParent = getParentNode(svgOffsetParent);\n }\n return win;\n }\n let offsetParent = getTrueOffsetParent(element, polyfill);\n while (offsetParent && isTableElement(offsetParent) && isStaticPositioned(offsetParent)) {\n offsetParent = getTrueOffsetParent(offsetParent, polyfill);\n }\n if (offsetParent && isLastTraversableNode(offsetParent) && isStaticPositioned(offsetParent) && !isContainingBlock(offsetParent)) {\n return win;\n }\n return offsetParent || getContainingBlock(element) || win;\n}\n\nconst getElementRects = async function (data) {\n const getOffsetParentFn = this.getOffsetParent || getOffsetParent;\n const getDimensionsFn = this.getDimensions;\n const floatingDimensions = await getDimensionsFn(data.floating);\n return {\n reference: getRectRelativeToOffsetParent(data.reference, await getOffsetParentFn(data.floating), data.strategy),\n floating: {\n x: 0,\n y: 0,\n width: floatingDimensions.width,\n height: floatingDimensions.height\n }\n };\n};\n\nfunction isRTL(element) {\n return getComputedStyle$1(element).direction === 'rtl';\n}\n\nconst platform = {\n convertOffsetParentRelativeRectToViewportRelativeRect,\n getDocumentElement,\n getClippingRect,\n getOffsetParent,\n getElementRects,\n getClientRects,\n getDimensions,\n getScale,\n isElement,\n isRTL\n};\n\nfunction rectsAreEqual(a, b) {\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;\n}\n\n// https://samthor.au/2021/observing-dom/\nfunction observeMove(element, onMove) {\n let io = null;\n let timeoutId;\n const root = getDocumentElement(element);\n function cleanup() {\n var _io;\n clearTimeout(timeoutId);\n (_io = io) == null || _io.disconnect();\n io = null;\n }\n function refresh(skip, threshold) {\n if (skip === void 0) {\n skip = false;\n }\n if (threshold === void 0) {\n threshold = 1;\n }\n cleanup();\n const elementRectForRootMargin = element.getBoundingClientRect();\n const {\n left,\n top,\n width,\n height\n } = elementRectForRootMargin;\n if (!skip) {\n onMove();\n }\n if (!width || !height) {\n return;\n }\n const insetTop = floor(top);\n const insetRight = floor(root.clientWidth - (left + width));\n const insetBottom = floor(root.clientHeight - (top + height));\n const insetLeft = floor(left);\n const rootMargin = -insetTop + \"px \" + -insetRight + \"px \" + -insetBottom + \"px \" + -insetLeft + \"px\";\n const options = {\n rootMargin,\n threshold: max(0, min(1, threshold)) || 1\n };\n let isFirstUpdate = true;\n function handleObserve(entries) {\n const ratio = entries[0].intersectionRatio;\n if (ratio !== threshold) {\n if (!isFirstUpdate) {\n return refresh();\n }\n if (!ratio) {\n // If the reference is clipped, the ratio is 0. Throttle the refresh\n // to prevent an infinite loop of updates.\n timeoutId = setTimeout(() => {\n refresh(false, 1e-7);\n }, 1000);\n } else {\n refresh(false, ratio);\n }\n }\n if (ratio === 1 && !rectsAreEqual(elementRectForRootMargin, element.getBoundingClientRect())) {\n // It's possible that even though the ratio is reported as 1, the\n // element is not actually fully within the IntersectionObserver's root\n // area anymore. This can happen under performance constraints. This may\n // be a bug in the browser's IntersectionObserver implementation. To\n // work around this, we compare the element's bounding rect now with\n // what it was at the time we created the IntersectionObserver. If they\n // are not equal then the element moved, so we refresh.\n refresh();\n }\n isFirstUpdate = false;\n }\n\n // Older browsers don't support a `document` as the root and will throw an\n // error.\n try {\n io = new IntersectionObserver(handleObserve, {\n ...options,\n // Handle