From b4eb2d2257a7493989ddee67932359c7cc27fe31 Mon Sep 17 00:00:00 2001 From: Volodymyr Makukha Date: Tue, 9 Sep 2025 13:18:33 +0100 Subject: [PATCH 1/4] Make blueprints previews adjustable to window height --- src/modules/add-site/components/blueprints.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/modules/add-site/components/blueprints.tsx b/src/modules/add-site/components/blueprints.tsx index 2155e5dc1..5619b4958 100644 --- a/src/modules/add-site/components/blueprints.tsx +++ b/src/modules/add-site/components/blueprints.tsx @@ -11,7 +11,7 @@ import { DataViews, View } from '@wordpress/dataviews'; import { sprintf } from '@wordpress/i18n'; import { Icon, external } from '@wordpress/icons'; import { useI18n } from '@wordpress/react-i18n'; -import { useCallback, useRef, useState, useMemo } from 'react'; +import { useCallback, useRef, useState, useMemo, useEffect } from 'react'; import StudioButton from 'src/components/button'; import { cx } from 'src/lib/cx'; import { getIpcApi } from 'src/lib/get-ipc-api'; @@ -58,6 +58,16 @@ export default function AddSiteBlueprint( { const { __ } = useI18n(); const fileRef = useRef< HTMLInputElement | null >( null ); const [ validationError, setValidationError ] = useState< string | null >( null ); + const [ windowHeight, setWindowHeight ] = useState( window.innerHeight ); + + useEffect( () => { + const handleResize = () => { + setWindowHeight( window.innerHeight ); + }; + + window.addEventListener( 'resize', handleResize ); + return () => window.removeEventListener( 'resize', handleResize ); + }, [] ); // Check if current selection is a file-based blueprint const isFileBasedSelection = selectedBlueprint && selectedBlueprint.startsWith( 'file:' ); @@ -104,7 +114,8 @@ export default function AddSiteBlueprint( { src={ item.image } alt={ item.title } className={ cx( - 'w-full h-32 object-cover object-top cursor-pointer transition-all duration-150 rounded-lg group', + 'w-full object-cover object-top cursor-pointer transition-all duration-150 rounded-lg group ', + windowHeight > 680 ? 'h-48' : 'h-32', 'hover:shadow-md hover:outline hover:outline-2 hover:outline-blue-500', 'transition-transform duration-150', 'hover:scale-105', @@ -200,7 +211,7 @@ export default function AddSiteBlueprint( { ), }, ], - [ blueprints, __ ] + [ blueprints, __, windowHeight ] ); const handleFileSelect = async ( event: React.ChangeEvent< HTMLInputElement > ) => { From 9f8e9ddbe7a48f4f92188041bd39abfbacef4b7a Mon Sep 17 00:00:00 2001 From: Volodymyr Makukha Date: Wed, 10 Sep 2025 09:31:44 +0100 Subject: [PATCH 2/4] chore: adjust --- .../add-site/components/blueprints.tsx | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/modules/add-site/components/blueprints.tsx b/src/modules/add-site/components/blueprints.tsx index 5619b4958..c5bacf5d8 100644 --- a/src/modules/add-site/components/blueprints.tsx +++ b/src/modules/add-site/components/blueprints.tsx @@ -58,16 +58,6 @@ export default function AddSiteBlueprint( { const { __ } = useI18n(); const fileRef = useRef< HTMLInputElement | null >( null ); const [ validationError, setValidationError ] = useState< string | null >( null ); - const [ windowHeight, setWindowHeight ] = useState( window.innerHeight ); - - useEffect( () => { - const handleResize = () => { - setWindowHeight( window.innerHeight ); - }; - - window.addEventListener( 'resize', handleResize ); - return () => window.removeEventListener( 'resize', handleResize ); - }, [] ); // Check if current selection is a file-based blueprint const isFileBasedSelection = selectedBlueprint && selectedBlueprint.startsWith( 'file:' ); @@ -114,8 +104,8 @@ export default function AddSiteBlueprint( { src={ item.image } alt={ item.title } className={ cx( - 'w-full object-cover object-top cursor-pointer transition-all duration-150 rounded-lg group ', - windowHeight > 680 ? 'h-48' : 'h-32', + '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', @@ -166,12 +156,12 @@ export default function AddSiteBlueprint( { const remainingCount = categories.length - MAX_BLUEPRINTS_CATEGORIES; return ( - + { visibleCategories.map( ( category ) => ( { category } @@ -211,7 +201,7 @@ export default function AddSiteBlueprint( { ), }, ], - [ blueprints, __, windowHeight ] + [ blueprints, __ ] ); const handleFileSelect = async ( event: React.ChangeEvent< HTMLInputElement > ) => { From d78e78f9b406581c179de65794474d1e5452124d Mon Sep 17 00:00:00 2001 From: Volodymyr Makukha Date: Wed, 10 Sep 2025 09:32:58 +0100 Subject: [PATCH 3/4] chore: remove leftovwers --- src/modules/add-site/components/blueprints.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/add-site/components/blueprints.tsx b/src/modules/add-site/components/blueprints.tsx index c5bacf5d8..f0cf8a7b3 100644 --- a/src/modules/add-site/components/blueprints.tsx +++ b/src/modules/add-site/components/blueprints.tsx @@ -11,7 +11,7 @@ import { DataViews, View } from '@wordpress/dataviews'; import { sprintf } from '@wordpress/i18n'; import { Icon, external } from '@wordpress/icons'; import { useI18n } from '@wordpress/react-i18n'; -import { useCallback, useRef, useState, useMemo, useEffect } from 'react'; +import { useCallback, useRef, useState, useMemo } from 'react'; import StudioButton from 'src/components/button'; import { cx } from 'src/lib/cx'; import { getIpcApi } from 'src/lib/get-ipc-api'; @@ -104,7 +104,7 @@ export default function AddSiteBlueprint( { src={ item.image } alt={ item.title } className={ cx( - 'w-full h-32 object-cover object-top cursor-pointer transition-all duration-150 rounded-lg group ', + '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', From 720e416d6df4ea3e1dbf736ff24d655e1701651d Mon Sep 17 00:00:00 2001 From: Bernardo Cotrim Date: Wed, 10 Sep 2025 17:30:43 +0100 Subject: [PATCH 4/4] Dynamic blueprints category overflow (#1757) * Add dynamic category overflow with smart +X more handling * fix magic numbers * add translation --- .../add-site/components/blueprints.tsx | 78 +++++++++++-------- .../add-site/hooks/use-overflow-items.ts | 77 ++++++++++++++++++ 2 files changed, 122 insertions(+), 33 deletions(-) create mode 100644 src/modules/add-site/hooks/use-overflow-items.ts diff --git a/src/modules/add-site/components/blueprints.tsx b/src/modules/add-site/components/blueprints.tsx index f0cf8a7b3..bcabdf66e 100644 --- a/src/modules/add-site/components/blueprints.tsx +++ b/src/modules/add-site/components/blueprints.tsx @@ -15,6 +15,7 @@ import { useCallback, useRef, useState, useMemo } from 'react'; import StudioButton from 'src/components/button'; import { cx } from 'src/lib/cx'; import { getIpcApi } from 'src/lib/get-ipc-api'; +import { useOverflowItems } from '../hooks/use-overflow-items'; interface Blueprint { slug: string; @@ -45,7 +46,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 default function AddSiteBlueprint( { blueprints, @@ -152,37 +194,7 @@ export default function AddSiteBlueprint( { 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 ; }, }, { @@ -355,7 +367,7 @@ export default function AddSiteBlueprint( { ) } -
+
{ errorMessage && ( { sprintf( __( 'Error loading featured blueprints: %s' ), errorMessage ) } 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, + }; +}