diff --git a/src/modules/add-site/components/blueprints.tsx b/src/modules/add-site/components/blueprints.tsx index 1d80e7926..5bd42f03b 100644 --- a/src/modules/add-site/components/blueprints.tsx +++ b/src/modules/add-site/components/blueprints.tsx @@ -17,6 +17,7 @@ import StudioButton from 'src/components/button'; import { cx } from 'src/lib/cx'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { useGetBlueprints } from 'src/stores/wpcom-api'; +import { useOverflowItems } from '../hooks/use-overflow-items'; interface Blueprint { slug: string; @@ -47,7 +48,48 @@ interface AddSiteBlueprintProps { onFileBlueprintSelect?: ( blueprint: Blueprint ) => void; } -const MAX_BLUEPRINTS_CATEGORIES = 3; +function CategoryBadges( { categories }: { categories: string[] } ) { + const { __ } = useI18n(); + const containerRef = useRef< HTMLDivElement >( null ); + const { visible, hidden, hiddenCount, itemRefs } = useOverflowItems( categories, containerRef ); + + return ( + + { categories.map( ( category, index ) => ( + { + itemRefs.current[ index ] = el; + } } + className="px-2.5 py-1 text-xs bg-gray-100 text-gray-700 rounded-sm flex items-center flex-shrink-0 max-w-32 truncate" + style={ { + visibility: index < visible.length ? 'visible' : 'hidden', + position: index >= visible.length ? 'absolute' : 'static', + } } + > + { category } + + ) ) } + { hiddenCount > 0 && ( + + + { /* translators: %d: Number of hidden categories */ } + { sprintf( __( '+%d more' ), hiddenCount ) } + + + ) } + + ); +} export function AddSiteBlueprintSelector( { blueprints, @@ -108,6 +150,7 @@ export function AddSiteBlueprintSelector( { alt={ item.title } className={ cx( 'w-full h-32 object-cover object-top cursor-pointer transition-all duration-150 rounded-lg group', + '[@media(min-height:680px)]:h-48', 'hover:shadow-md hover:outline hover:outline-2 hover:outline-blue-500', 'transition-transform duration-150', 'hover:scale-105', @@ -154,37 +197,7 @@ export function AddSiteBlueprintSelector( { const categories = ( item.blueprint.meta?.categories || [] ).filter( ( category ) => category !== 'Studio' ); - const visibleCategories = categories.slice( 0, MAX_BLUEPRINTS_CATEGORIES ); - const remainingCount = categories.length - MAX_BLUEPRINTS_CATEGORIES; - - return ( - - { visibleCategories.map( ( category ) => ( - - { category } - - ) ) } - { remainingCount > 0 && ( - - - +{ remainingCount } more - - - ) } - - ); + return ; }, }, { @@ -357,7 +370,7 @@ export function AddSiteBlueprintSelector( { ) } -
+
{ isFetchingBlueprints && ( { __( 'Loading blueprints...' ) } diff --git a/src/modules/add-site/hooks/use-overflow-items.ts b/src/modules/add-site/hooks/use-overflow-items.ts new file mode 100644 index 000000000..d758f658c --- /dev/null +++ b/src/modules/add-site/hooks/use-overflow-items.ts @@ -0,0 +1,77 @@ +import { useLayoutEffect, useState, useRef, useCallback } from 'react'; + +// Constants for layout calculations +const ITEM_SPACING = 12; // Gap between category items in pixels +const MORE_BUTTON_WIDTH = 70; // Estimated width of "+X more" button in pixels +const DEBOUNCE_DELAY = 50; // Delay in milliseconds for debounced recalculation +const TOLERANCE = 10; // Extra pixels to account for measurement precision + +export function useOverflowItems( items: string[], containerRef: React.RefObject< HTMLElement > ) { + const [ visibleCount, setVisibleCount ] = useState( items.length ); + const itemRefs = useRef< ( HTMLElement | null )[] >( [] ); + const lastWidth = useRef( 0 ); + const debounceTimeout = useRef< NodeJS.Timeout | null >( null ); + + const calculate = useCallback( () => { + if ( ! containerRef.current || items.length === 0 ) { + return; + } + + const container = containerRef.current; + const containerWidth = container.offsetWidth; + + if ( containerWidth === 0 || containerWidth === lastWidth.current ) { + return; + } + + lastWidth.current = containerWidth; + + let totalWidth = 0; + let count = 0; + + for ( let i = 0; i < itemRefs.current.length; i++ ) { + const item = itemRefs.current[ i ]; + if ( ! item ) continue; + + const itemWidth = item.offsetWidth; + const spacing = i > 0 ? ITEM_SPACING : 0; + const moreButtonSpace = i < items.length - 1 ? MORE_BUTTON_WIDTH : 0; + + if ( totalWidth + spacing + itemWidth + moreButtonSpace <= containerWidth ) { + totalWidth += spacing + itemWidth; + count = i + 1; + } else { + break; + } + } + + if ( count === items.length - 1 && itemRefs.current[ items.length - 1 ] ) { + const lastItem = itemRefs.current[ items.length - 1 ]; + const lastItemWidth = lastItem?.offsetWidth || 0; + if ( totalWidth + ITEM_SPACING + lastItemWidth <= containerWidth + TOLERANCE ) { + count = items.length; + } + } + + setVisibleCount( Math.max( 1, count ) ); + }, [ items, containerRef ] ); + + useLayoutEffect( () => { + if ( debounceTimeout.current ) { + clearTimeout( debounceTimeout.current ); + } + debounceTimeout.current = setTimeout( calculate, DEBOUNCE_DELAY ); + return () => { + if ( debounceTimeout.current ) { + clearTimeout( debounceTimeout.current ); + } + }; + }, [ calculate ] ); + + return { + visible: items.slice( 0, visibleCount ), + hidden: items.slice( visibleCount ), + hiddenCount: items.length - visibleCount, + itemRefs, + }; +}