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,
+ };
+}