From 178be9bb4b7959ac5f4a81d76e483c728e52821e Mon Sep 17 00:00:00 2001 From: Ionut Neagu Date: Sun, 21 Jun 2026 06:42:34 +0200 Subject: [PATCH 1/9] feat(starter-ranking): send site title; disclose external service in readme - Send the site title (blogname) alongside the URL in the AI starter-site ranking request so the service has a niche signal beyond the URL. - Add a readme "External services" section disclosing the ThemeIsle starter-sites service (catalog, ranking, license) per the WordPress.org plugin guidelines. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BNzxtv8VLmNXCCqQyK4gUC --- includes/Starter_Ranking.php | 5 +++-- readme.txt | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/includes/Starter_Ranking.php b/includes/Starter_Ranking.php index 45032577..19bc8a6c 100644 --- a/includes/Starter_Ranking.php +++ b/includes/Starter_Ranking.php @@ -89,8 +89,9 @@ private static function fetch_order( $builder, $query = null, $budget = null ) { $deadline = time() + ( null !== $budget ? (int) $budget : self::REQUEST_BUDGET ); $start = self::base_url() . '/api/workflows/' . self::SLUG . '/start'; $body = array( - 'site_url' => home_url(), - 'builder' => $builder, + 'site_url' => home_url(), + 'site_title' => sanitize_text_field( get_bloginfo( 'name' ) ), + 'builder' => $builder, ); if ( null !== $query && '' !== $query ) { diff --git a/readme.txt b/readme.txt index 41cf11cc..42cdf0d2 100644 --- a/readme.txt +++ b/readme.txt @@ -28,6 +28,13 @@ You can check the full collection on [demosites.io](https://demosites.io/) = Can I import just one page from one starter site? = Yes, you can choose to import either one template or the entire starter site. +== External services == + +The Starter Sites & Templates library is powered by a cloud service run by ThemeIsle. This section explains what data is sent to it and when. By using these features you agree to ThemeIsle's [Terms](https://themeisle.com/terms/) and [Privacy Policy](https://themeisle.com/privacy-policy/). No account is required to browse the free starter-sites collection. + += ThemeIsle starter-sites service (api.themeisle.com) = +The plugin contacts ThemeIsle's service to fetch the starter-sites and templates catalog, to order it by relevance to your site (and to run searches), and to validate your license. For relevance ordering and search it sends your site address (home URL), your site title, the page builder you use, and — when you search — your search text; if you have a ThemeIsle license, it also sends your license key. These requests are made when you browse or search the library, and apply to local and staging sites too. Service provided by ThemeIsle — [Terms](https://themeisle.com/terms/), [Privacy Policy](https://themeisle.com/privacy-policy/). + == Changelog == #### [Version 1.3.0](https://github.com/Codeinwp/templates-patterns-collection/compare/v1.2.29...v1.3.0) (2026-06-18) From b0a429a51f837805c13ba987f2e620cbe18f0fc5 Mon Sep 17 00:00:00 2001 From: Ionut Neagu Date: Mon, 22 Jun 2026 16:00:27 +0200 Subject: [PATCH 2/9] feat(onboarding): show 4 starter sites per row on laptop and up Widen the onboarding starter-sites grid from 3 to 4 columns at the laptop breakpoint (>=992px) so users see ~8 templates at once instead of leaving large empty margins on wider screens. Tablet stays at 2 columns, mobile at 1. Keep the JS in sync with the new column count: - Page size 9 -> 12 (initial useState, the reset effect, and the lazy-load increment) so each render fills whole rows. 12 divides evenly by both 3 and 4, so rows stay full at every breakpoint. - Search "Browse more" divider padding % 3 -> % 4 so the divider lands on a clean row boundary under the 4-column grid. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QrGf4K9upxDKhRPVCztoLs --- onboarding/src/Components/Sites.js | 10 +++++----- onboarding/src/scss/_general.scss | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/onboarding/src/Components/Sites.js b/onboarding/src/Components/Sites.js index a0ef9747..efee2918 100644 --- a/onboarding/src/Components/Sites.js +++ b/onboarding/src/Components/Sites.js @@ -27,11 +27,11 @@ const Sites = ( { sortBy, selectedColors, } ) => { - const [ maxShown, setMaxShown ] = useState( 9 ); + const [ maxShown, setMaxShown ] = useState( 12 ); const { sites = {} } = getSites; useEffect( () => { - setMaxShown( 9 ); + setMaxShown( 12 ); }, [ editor, category, searchQuery, sortBy, selectedColors ] ); const getBuilders = () => Object.keys( sites ); @@ -218,9 +218,9 @@ const Sites = ( { const rest = filterByColors( filterByCategory( ranked, category ) ).filter( ( site ) => site && site.slug && ! inMatches[ site.slug ] ); - const remainder = matches.length % 3; + const remainder = matches.length % 4; const pad = - remainder === 0 ? 0 : Math.min( 3 - remainder, rest.length ); + remainder === 0 ? 0 : Math.min( 4 - remainder, rest.length ); return { list: [ ...matches, ...rest ], @@ -280,7 +280,7 @@ const Sites = ( { return false; } - setMaxShown( ( shown ) => shown + 9 ); + setMaxShown( ( shown ) => shown + 12 ); } } > Date: Mon, 22 Jun 2026 16:45:56 +0200 Subject: [PATCH 3/9] feat(onboarding): tone down starter-sites listing banners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two banners on the "Choose a design" step were too intrusive. 1. Remove the full-width "80+ new starter sites" promo notice (added in #465) and keep the social proof as a quiet subtitle under the heading: "Nearly 200 starter sites for every niche — fresh designs added regularly." Removes the OnboardingPromoNotice component + SCSS, the SiteList mount, the style import, and the dismiss AJAX handler / show-logic / localization in Admin.php, plus the e2e dismiss test. 2. Make the premium-upsell toast compact (smaller padding/font, capped width, smaller logo, no fixed 70px height) and sharpen the copy: "Unlock every premium template with Neve Business. Upgrade" (was "Unlock Access to all premium templates with Neve Business plan. Get Started."). Note: should_show_business_agency_promo_text() + the onboardingShowProNoticeText localization are now unused (they only varied the removed notice's text); left in place with their unit tests and can be pruned in a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QrGf4K9upxDKhRPVCztoLs --- e2e-tests/specs/onboarding.spec.js | 35 ------ includes/Admin.php | 53 --------- .../src/Components/OnboardingPromoNotice.js | 96 ---------------- onboarding/src/Components/Steps/SiteList.js | 23 ++-- onboarding/src/scss/_general.scss | 19 ++++ .../src/scss/_onboarding-promo-notice.scss | 105 ------------------ onboarding/src/scss/_toast.scss | 34 ++++-- onboarding/src/style.scss | 1 - 8 files changed, 60 insertions(+), 306 deletions(-) delete mode 100644 onboarding/src/Components/OnboardingPromoNotice.js delete mode 100644 onboarding/src/scss/_onboarding-promo-notice.scss diff --git a/e2e-tests/specs/onboarding.spec.js b/e2e-tests/specs/onboarding.spec.js index d737ce93..81a3c35b 100644 --- a/e2e-tests/specs/onboarding.spec.js +++ b/e2e-tests/specs/onboarding.spec.js @@ -62,41 +62,6 @@ test.describe('Onboarding', () => { await expect(firstListedSiteCard.locator('.ss-title')).not.toBeEmpty(); }); - test('Onboarding promo notice can be dismissed and stays hidden after reload', async ({ page, admin }) => { - await admin.visitAdminPage(ONBOARDING_URL); - - const promoNotice = page.locator('.ob-onboarding-promo'); - await expect(promoNotice).toBeVisible(); - - // The client sends the dismiss action via FormData, which fetch encodes as - // multipart/form-data. Inspect the raw body bytes so the predicate works - // regardless of whether the encoding is multipart or url-encoded. - const isDismissCall = (request) => - request.url().includes('admin-ajax.php') && - request.method() === 'POST' && - (request.postDataBuffer()?.toString('utf8') ?? '').includes( - 'dismiss_onboarding_promo_notice' - ); - - const dismissRequest = page.waitForRequest(isDismissCall); - const dismissResponse = page.waitForResponse((response) => - isDismissCall(response.request()) - ); - - await promoNotice.getByRole('button', { name: 'Dismiss notice' }).click(); - - const request = await dismissRequest; - const response = await dismissResponse; - expect(request.postDataBuffer()?.toString('utf8')).toContain( - 'dismiss_onboarding_promo_notice' - ); - expect(response.ok()).toBeTruthy(); - - await expect(promoNotice).toBeHidden(); - await page.reload(); - await expect(promoNotice).toBeHidden(); - }); - test('Site Import Customization Rendering', async ({ page, admin }) => { await admin.visitAdminPage(ONBOARDING_URL); await openFirstSiteAndWaitForData( page ); diff --git a/includes/Admin.php b/includes/Admin.php index 625c1a2d..9680c026 100755 --- a/includes/Admin.php +++ b/includes/Admin.php @@ -26,7 +26,6 @@ class Admin { const TC_REMOVED_KEY = 'tiob_tc_removed'; const TC_NEW_NOTICE_DISMISSED = 'tiob_new_tc_notice_dismissed'; - const ONBOARDING_PROMO_NOTICE_DISMISSED = 'tiob_onboarding_promo_notice_dismissed'; const VISITED_LIBRARY_OPT = 'tiob_library_visited'; /** @@ -89,7 +88,6 @@ public function init() { add_action( 'wp_ajax_tpc_get_logs', array( $this, 'external_get_logs' ) ); add_action( 'wp_ajax_dismiss_new_tc_notice', array( $this, 'dismiss_new_tc_notice' ) ); - add_action( 'wp_ajax_dismiss_onboarding_promo_notice', array( $this, 'dismiss_onboarding_promo_notice' ) ); $this->register_feedback_settings(); @@ -163,52 +161,6 @@ public function dismiss_new_tc_notice() { $this->ensure_ajax_response( $response ); } - /** - * Dismiss onboarding promo notice. - * - * @return void - */ - public function dismiss_onboarding_promo_notice() { - $response = array( - 'success' => false, - 'code' => 'ti__ob_not_allowed', - 'message' => 'Not allowed!', - ); - - if ( ! isset( $_REQUEST['nonce'] ) ) { - $this->ensure_ajax_response( $response ); - return; - } - - $nonce = sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ); - - if ( ! wp_verify_nonce( $nonce, 'dismiss_onboarding_promo_notice' ) ) { - $this->ensure_ajax_response( $response ); - return; - } - - if ( ! current_user_can( 'install_plugins' ) ) { - $this->ensure_ajax_response( $response ); - return; - } - - $response['success'] = true; - unset( $response['code'] ); - unset( $response['message'] ); - - update_option( self::ONBOARDING_PROMO_NOTICE_DISMISSED, 'yes' ); - $this->ensure_ajax_response( $response ); - } - - /** - * Decide if the onboarding promo notice should be shown. - * - * @return bool - */ - private function should_show_onboarding_promo_notice() { - return get_option( self::ONBOARDING_PROMO_NOTICE_DISMISSED, 'no' ) !== 'yes'; - } - /** * Decide if the business/agency variant of the onboarding promo text should be shown. * @@ -937,11 +889,6 @@ private function get_localization() { 'ajaxURL' => esc_url( admin_url( 'admin-ajax.php' ) ), 'nonce' => wp_create_nonce( 'dismiss_new_tc_notice' ), ), - 'onboardingPromoNotice' => array( - 'show' => $this->should_show_onboarding_promo_notice(), - 'ajaxURL' => esc_url( admin_url( 'admin-ajax.php' ) ), - 'nonce' => wp_create_nonce( 'dismiss_onboarding_promo_notice' ), - ), 'onboardingPluginCompatibility' => array( 'hyve-lite' => is_php_version_compatible( '8.1' ), ), diff --git a/onboarding/src/Components/OnboardingPromoNotice.js b/onboarding/src/Components/OnboardingPromoNotice.js deleted file mode 100644 index 9ddfee31..00000000 --- a/onboarding/src/Components/OnboardingPromoNotice.js +++ /dev/null @@ -1,96 +0,0 @@ -/* global tiobDash */ -import { __, sprintf } from '@wordpress/i18n'; -import { createInterpolateElement, useState } from '@wordpress/element'; -import { ajaxAction } from '../utils/rest'; - -const OnboardingPromoNotice = () => { - const shouldShowNotice = Boolean( tiobDash.onboardingPromoNotice?.show ); - const showProMessage = Boolean( tiobDash.onboardingShowProNoticeText ); - - const emailBody = sprintf( - /* translators: %s: double line break in the starter site request email template */ - __( - 'Hi Neve team,%1$sI\'m looking for a starter site for the following project:%1$sProject type: (e.g. Restaurant, Law Firm, SaaS)%1$sKey pages needed: (e.g. Home, About, Services, Contact)%1$sStyle preference: (e.g. Minimal, Bold, Corporate)%1$sAny references: (optional)%1$sThanks', - 'templates-patterns-collection' - ), - '\n\n' - ); - - const requestSiteLink = - 'mailto:contact@themeisle.com?subject=' + - encodeURIComponent( - __( 'Starter Site Request', 'templates-patterns-collection' ) - ) + - '&body=' + - encodeURIComponent( emailBody ); - - const noticeMessage = showProMessage - ? createInterpolateElement( - __( - 'Fresh designs built for every niche. Can\'t find what you\'re looking for? As a Pro user, request a site and we\'ll build it for you.', - 'templates-patterns-collection' - ), - { - requestSiteLink: ( - /* eslint-disable-next-line jsx-a11y/anchor-has-content */ - - ), - } - ) - : __( - 'From free to pro, fresh designs built for every niche. More coming soon.', - 'templates-patterns-collection' - ); - - const [ isVisible, setIsVisible ] = useState( shouldShowNotice ); - - if ( ! isVisible ) { - return null; - } - - const dismissNotice = () => { - setIsVisible( false ); - ajaxAction( - tiobDash.onboardingPromoNotice.ajaxURL, - 'dismiss_onboarding_promo_notice', - tiobDash.onboardingPromoNotice.nonce - ).catch( () => null ); - }; - - return ( -
-
- { __( 'New', 'templates-patterns-collection' ) } -
-
-

- { sprintf( - /* translators: %s: number of new starter sites */ - __( - '%s new starter sites, just landed.', - 'templates-patterns-collection' - ), - '80+' - ) } -

-

{ noticeMessage }

-
- -
- ); -}; - -export default OnboardingPromoNotice; diff --git a/onboarding/src/Components/Steps/SiteList.js b/onboarding/src/Components/Steps/SiteList.js index 563b4827..fa4d3367 100644 --- a/onboarding/src/Components/Steps/SiteList.js +++ b/onboarding/src/Components/Steps/SiteList.js @@ -11,7 +11,6 @@ import Toast from '../Toast'; import Filters from '../Filters'; import Sites from '../Sites'; import EditorSelector from '../EditorSelector'; -import OnboardingPromoNotice from '../OnboardingPromoNotice'; import SVG from '../../utils/svg'; import { get, track } from '../../utils/rest'; @@ -41,7 +40,7 @@ const SiteList = ( { const toastMessage = createInterpolateElement( __( - 'Unlock Access to all premium templates with Neve Business plan.
.', + 'Unlock every premium template with Neve Business. ', 'templates-patterns-collection' ), { @@ -51,7 +50,7 @@ const SiteList = ( { target="_blank" rel="external noreferrer noopener" > - { __( 'Get Started', 'templates-patterns-collection' ) } + { __( 'Upgrade', 'templates-patterns-collection' ) } ), } @@ -211,13 +210,23 @@ const SiteList = ( {
-

- { __( 'Choose a design', 'templates-patterns-collection' ) } -

+
+

+ { __( + 'Choose a design', + 'templates-patterns-collection' + ) } +

+

+ { __( + 'Nearly 200 starter sites for every niche — fresh designs added regularly.', + 'templates-patterns-collection' + ) } +

+
- { ( personalizing || searching ) && (
Date: Mon, 22 Jun 2026 16:56:31 +0200 Subject: [PATCH 4/9] fix(onboarding): hide color picker when a starter site has a single palette The per-card color swatch summary rendered whenever a site had at least one color, showing a lone non-actionable dot when there was nothing to choose. Only render the picker when there are 2+ palettes (colorOptions.length > 1). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QrGf4K9upxDKhRPVCztoLs --- onboarding/src/Components/StarterSiteCard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onboarding/src/Components/StarterSiteCard.js b/onboarding/src/Components/StarterSiteCard.js index f4f6849d..7d17c12d 100644 --- a/onboarding/src/Components/StarterSiteCard.js +++ b/onboarding/src/Components/StarterSiteCard.js @@ -194,7 +194,7 @@ const StarterSiteCard = ( { ) ) }
) } - { colorOptions.length > 0 && ( + { colorOptions.length > 1 && (
Date: Mon, 22 Jun 2026 17:20:41 +0200 Subject: [PATCH 5/9] refactor(onboarding): reviewed notice designs + logo scaling + phpcs fix Applies the workflow-decided implementations for the two onboarding notices, verified in a local wp-env Neve onboarding instance. - Subtitle: copy -> "Nearly 200 starter sites across every niche, with dozens added recently." (truthful, evergreen). Fix a WCAG AA contrast failure: $secondary-text #848484 (~3.5:1) -> mix(...) ~#6E6D6D (~4.6:1); subtle weight-only emphasis on the count, plus a small-screen scale step. - Upsell toast: two-line copy ("Unlock every premium template" / "Included with Neve Business. "), white card, Neve mark scaled to 24px, role=status + accessible (type=button, aria-label) dismiss, focus-visible rings, reduced-motion, mobile full-width strip. - Logo: add viewBox="0 0 40 40" to SVG.logo so it scales instead of cropping when constrained below 40px (fixes the clipped mark in the toast). - phpcs: re-align the const block after the promo-notice constant removal. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QrGf4K9upxDKhRPVCztoLs --- includes/Admin.php | 6 +- onboarding/src/Components/Steps/SiteList.js | 25 ++-- onboarding/src/Components/Toast.js | 27 +++- onboarding/src/scss/_general.scss | 20 ++- onboarding/src/scss/_toast.scss | 130 ++++++++++++++------ onboarding/src/utils/svg.js | 1 + 6 files changed, 151 insertions(+), 58 deletions(-) diff --git a/includes/Admin.php b/includes/Admin.php index 9680c026..e8cd0ac9 100755 --- a/includes/Admin.php +++ b/includes/Admin.php @@ -24,9 +24,9 @@ class Admin { const IMPORTED_TEMPLATES_COUNT_OPT = 'tiob_premade_imported'; const FEEDBACK_DISMISSED_OPT = 'tiob_feedback_dismiss'; - const TC_REMOVED_KEY = 'tiob_tc_removed'; - const TC_NEW_NOTICE_DISMISSED = 'tiob_new_tc_notice_dismissed'; - const VISITED_LIBRARY_OPT = 'tiob_library_visited'; + const TC_REMOVED_KEY = 'tiob_tc_removed'; + const TC_NEW_NOTICE_DISMISSED = 'tiob_new_tc_notice_dismissed'; + const VISITED_LIBRARY_OPT = 'tiob_library_visited'; /** * Admin page slug diff --git a/onboarding/src/Components/Steps/SiteList.js b/onboarding/src/Components/Steps/SiteList.js index fa4d3367..f59700b6 100644 --- a/onboarding/src/Components/Steps/SiteList.js +++ b/onboarding/src/Components/Steps/SiteList.js @@ -40,7 +40,7 @@ const SiteList = ( { const toastMessage = createInterpolateElement( __( - 'Unlock every premium template with Neve Business. ', + 'Included with Neve Business. See plans', 'templates-patterns-collection' ), { @@ -48,10 +48,8 @@ const SiteList = ( { - { __( 'Upgrade', 'templates-patterns-collection' ) } - + rel="noopener noreferrer" + /> ), } ); @@ -218,9 +216,16 @@ const SiteList = ( { ) }

- { __( - 'Nearly 200 starter sites for every niche — fresh designs added regularly.', - 'templates-patterns-collection' + { createInterpolateElement( + __( + 'Nearly 200 starter sites across every niche, with dozens added recently.', + 'templates-patterns-collection' + ), + { + count: ( + + ), + } ) }

@@ -257,6 +262,10 @@ const SiteList = ( { setShowToast={ setShowToast } svgIcon={ SVG.logo } className={ showToast === true ? 'show' : '' } + heading={ __( + 'Unlock every premium template', + 'templates-patterns-collection' + ) } message={ toastMessage } /> ) } diff --git a/onboarding/src/Components/Toast.js b/onboarding/src/Components/Toast.js index e65be8f6..8e619675 100644 --- a/onboarding/src/Components/Toast.js +++ b/onboarding/src/Components/Toast.js @@ -1,18 +1,33 @@ import classnames from 'classnames'; +import { __ } from '@wordpress/i18n'; -const Toast = ( { svgIcon, message, className, setShowToast } ) => { +const Toast = ( { svgIcon, heading, message, className, setShowToast } ) => { const handleClose = () => { setShowToast( 'dismissed' ); }; return ( -
+
+ { svgIcon && ( + + ) }
- { svgIcon &&
{ svgIcon }
} - { message &&

{ message }

} + { heading &&

{ heading }

} + { message &&

{ message }

}
-
); diff --git a/onboarding/src/scss/_general.scss b/onboarding/src/scss/_general.scss index 3784d42d..0e5b0abc 100644 --- a/onboarding/src/scss/_general.scss +++ b/onboarding/src/scss/_general.scss @@ -149,11 +149,27 @@ iframe { } .ob-subtitle { - margin: 0; - color: $secondary-text; + margin: 6px 0 0; + max-width: 56ch; + // ~#6E6D6D on white ≈ 4.6:1 → passes WCAG AA while staying clearly secondary to the near-black h1. + color: mix($main-text, $inverted-text, 56%); font-size: 14px; line-height: 20px; font-weight: 400; + letter-spacing: 0.1px; + + // Subtle emphasis on the catalog claim — weight only, no brand color (would read promotional). + .ob-subtitle__count { + font-weight: 600; + color: $main-text; + } +} + +@media (max-width: #{$tablet}) { + .ob-subtitle { + font-size: 13px; + line-height: 19px; + } } /** diff --git a/onboarding/src/scss/_toast.scss b/onboarding/src/scss/_toast.scss index 996e42e3..4b481153 100644 --- a/onboarding/src/scss/_toast.scss +++ b/onboarding/src/scss/_toast.scss @@ -1,66 +1,118 @@ .ob-toast { position: fixed; bottom: 24px; - right: 18px; - opacity: 0; - transform: translateY(20px); /* Start 20px below the final position */ - transition: opacity 0.5s ease, transform 0.5s ease; /* Add transition */ - display: inline-flex; - padding: 10px 14px; - align-items: center; + right: 24px; + z-index: 100; + display: flex; + align-items: flex-start; gap: 10px; - flex-shrink: 0; max-width: 340px; + padding: 12px 14px; border-radius: $button-radius; border: 1px solid $border; - background: $light-bg; - box-shadow: 4px 4px 24px 0px rgba(0, 0, 0, 0.18); - font-size: 13px; - font-style: normal; - font-weight: 400; + background: #fff; + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12); + opacity: 0; + transform: translateY(12px); + transition: opacity 0.35s ease, transform 0.35s ease; + pointer-events: none; } -.ob-toast-content { - display: flex; - flex: 1; - align-items: center; - gap: 10px; - p { - margin: 0; - color: $main-text; - font-size: 13px; - line-height: 18px; +.ob-toast.show { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +.ob-toast-icon { + flex: 0 0 auto; + line-height: 0; + margin-top: 1px; + + svg { + width: 24px; + height: 24px; + display: block; } +} + +.ob-toast-content { + flex: 1 1 auto; + min-width: 0; +} + +.ob-toast-heading { + margin: 0; + color: $main-text; + font-size: 13px; + line-height: 18px; + font-weight: 600; +} + +.ob-toast-message { + margin: 2px 0 0; + color: $secondary-text; + font-size: 13px; + line-height: 18px; + font-weight: 400; + a { - text-decoration: none; color: $primary; - font-weight: 700; + font-weight: 600; + text-decoration: none; white-space: nowrap; - } -} -.ob-toast-icon { - display: inline-flex; - flex-shrink: 0; + &:hover, + &:focus-visible { + text-decoration: underline; + } - svg { - width: 22px; - height: 22px; + &:focus-visible { + outline: 2px solid $primary; + outline-offset: 2px; + border-radius: 2px; + } } } .ob-toast-close { + flex: 0 0 auto; + margin: -4px -4px 0 2px; + padding: 4px; background: none; border: none; - font-size: 18px; line-height: 1; + font-size: 16px; color: $secondary-text; cursor: pointer; - flex-shrink: 0; + border-radius: 4px; + + &:hover { + color: $main-text; + } + + &:focus-visible { + outline: 2px solid $primary; + outline-offset: 1px; + } } -/* Add a class to control the appearance */ -.ob-toast.show { - opacity: 1; /* Show with full opacity */ - transform: translateY(0); /* Move to the final position */ +@media (prefers-reduced-motion: reduce) { + .ob-toast { + transition: opacity 0.2s ease; + transform: none; + } + + .ob-toast.show { + transform: none; + } +} + +@media (max-width: #{$tablet}) { + .ob-toast { + left: 16px; + right: 16px; + bottom: 16px; + max-width: none; + } } diff --git a/onboarding/src/utils/svg.js b/onboarding/src/utils/svg.js index 3985f31e..0fa1308b 100644 --- a/onboarding/src/utils/svg.js +++ b/onboarding/src/utils/svg.js @@ -4,6 +4,7 @@ const SVG = { xmlns="http://www.w3.org/2000/svg" width="40" height="40" + viewBox="0 0 40 40" fill="none" > Date: Mon, 22 Jun 2026 17:23:07 +0200 Subject: [PATCH 6/9] test(onboarding): match All/Free filter buttons exactly The grid now renders a full page of 12 cards, so card page-shot buttons (e.g. "Gallery", "Ballet Blog", "All Courses") contain "all" as a substring. The non-exact getByRole('button', { name: 'All' }) locator then resolved to multiple elements (Playwright strict mode violation). Match the All/Free filter buttons by exact name. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01GYVWBEDjCM3wzBP6Ew1Ku4 --- e2e-tests/specs/onboarding.spec.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/e2e-tests/specs/onboarding.spec.js b/e2e-tests/specs/onboarding.spec.js index d737ce93..fbbf6673 100644 --- a/e2e-tests/specs/onboarding.spec.js +++ b/e2e-tests/specs/onboarding.spec.js @@ -48,9 +48,12 @@ test.describe('Onboarding', () => { expect(await page.locator('.ss-card .ss-badge').count()).toBeGreaterThan(0); // 'All' and 'Free' should show after you select a category. + // Match exactly: card page-shot buttons (e.g. "Gallery", "Ballet Blog", + // "All Courses") also contain "all" as a substring, so a non-exact name + // match resolves to multiple elements once the grid renders a full page. await page.locator('.ob-cat-wrap').first().click(); - await expect(page.getByRole('button', { name: 'All' })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Free' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'All', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Free', exact: true })).toBeVisible(); // Check card structure. const firstListedSiteCard = page.locator('.ss-card-wrap').first(); From b3b966387bcd955726afc5a99567248ee63a4498 Mon Sep 17 00:00:00 2001 From: Ionut Neagu Date: Mon, 22 Jun 2026 17:26:30 +0200 Subject: [PATCH 7/9] fix(onboarding): sync card screenshot to the selected color filter When a color is selected in the global filter, each matching starter-site card now switches its screenshot to that color's palette (screenshots_by_color), restoring the intended filter behavior (the card previously stayed on its default palette). All in StarterSiteCard: - subscribe to getSelectedColors via withSelect; - derive `filterColor` = the first selected color the card actually has (case-insensitive match, returns the raw colorOptions slug); - seed activeColor from filterColor (falling back to the first palette) in an effect keyed on [colorOptions, filterColor], so manual swatch clicks survive until the filter changes and a cleared filter reverts to the default. Falls back to the base screenshot when screenshots_by_color lacks the selected color. Builds on the single-palette swatch gate (colorOptions.length > 1). Verified in a local wp-env Neve onboarding instance: selecting "green" switched the matching cards to their -green screenshots. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QrGf4K9upxDKhRPVCztoLs --- onboarding/src/Components/StarterSiteCard.js | 38 ++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/onboarding/src/Components/StarterSiteCard.js b/onboarding/src/Components/StarterSiteCard.js index 7d17c12d..6b292d3a 100644 --- a/onboarding/src/Components/StarterSiteCard.js +++ b/onboarding/src/Components/StarterSiteCard.js @@ -47,7 +47,8 @@ const StarterSiteCard = ( { setSite, handleNextStep, trackingId, - editor + editor, + selectedColors = [] } ) => { const { upsell, @@ -93,6 +94,32 @@ const StarterSiteCard = ( { screenshot: screenshotMap[ color ] || screenshot, } ) ); }, [ data?.colors, data?.screenshots_by_color, screenshot ] ); + // First globally-selected color this card actually has, returned as the + // canonical RAW colorOptions slug so option.slug === activeColor still hits. + // selectedColors are normalized lowercase (Filters.js) while colorOptions + // slugs / screenshots_by_color keys are raw, so match case-insensitively. + const filterColor = useMemo( () => { + if ( + ! Array.isArray( selectedColors ) || + ! selectedColors.length || + ! colorOptions.length + ) { + return ''; + } + + const normalizedToSlug = new Map( + colorOptions.map( ( option ) => [ + String( option.slug || '' ).trim().toLowerCase(), + option.slug, + ] ) + ); + + const matched = selectedColors.find( ( color ) => + normalizedToSlug.has( color ) + ); + + return matched ? normalizedToSlug.get( matched ) : ''; + }, [ selectedColors, colorOptions ] ); const previewColors = colorOptions.slice( 0, 2 ); const [ activePage, setActivePage ] = useState( pageShots[0]?.key || 'home' ); const [ activeColor, setActiveColor ] = useState( colorOptions[0]?.slug || '' ); @@ -102,9 +129,12 @@ const StarterSiteCard = ( { setActivePage( pageShots[0]?.key || 'home' ); }, [ pageShots ] ); + // Seed the displayed color from the global filter when this card matches a + // selected color; otherwise fall back to the first palette. Re-runs only on + // filter/colorOptions changes, so manual swatch clicks survive until then. useEffect( () => { - setActiveColor( colorOptions[0]?.slug || '' ); - }, [ colorOptions ] ); + setActiveColor( filterColor || colorOptions[0]?.slug || '' ); + }, [ colorOptions, filterColor ] ); const activePageShot = pageShots.find( ( shot ) => shot.key === activePage ) || pageShots[0] || null; @@ -290,12 +320,14 @@ export default compose( getCurrentEditor, getCurrentCategory, getSearchQuery, + getSelectedColors, } = select( 'ti-onboarding' ); return { trackingId: getTrackingId(), editor: getCurrentEditor(), category: getCurrentCategory(), query: getSearchQuery(), + selectedColors: getSelectedColors() || [], }; } ), withDispatch( ( dispatch, { data } ) => { From 8f0424a375f0f7df527a53ded62d333383930052 Mon Sep 17 00:00:00 2001 From: Ionut Neagu Date: Mon, 22 Jun 2026 17:40:48 +0200 Subject: [PATCH 8/9] release(minor): starter-sites onboarding improvements - Starter sites now show 4 per row on laptop and desktop, so you can see more designs at a glance. - Tidier library: the promotional banner is replaced with a subtle subtitle, and the upgrade prompt is smaller with clearer wording. - Color variants: select a color to preview every matching starter site in that palette; sites with a single palette no longer show an empty color control. - Smarter results: your site title is now used to better order and search the starter-sites library. From 0c16ced7d7ece8f457ba277d0b51fa1a73750bc0 Mon Sep 17 00:00:00 2001 From: Ionut Neagu Date: Mon, 22 Jun 2026 17:53:07 +0200 Subject: [PATCH 9/9] fix(onboarding): reveal the color swatch only on card hover The per-card color summary showed at rest, adding visual noise to the grid. Hide it at rest and reveal it on card hover/focus (matching the page-shot tabs) so the resting grid stays clean; the full white pill still appears on hover. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QrGf4K9upxDKhRPVCztoLs --- onboarding/src/scss/_starter-site-card.scss | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/onboarding/src/scss/_starter-site-card.scss b/onboarding/src/scss/_starter-site-card.scss index a5a68540..a5ba2ea6 100644 --- a/onboarding/src/scss/_starter-site-card.scss +++ b/onboarding/src/scss/_starter-site-card.scss @@ -46,6 +46,12 @@ transform: translateY(0); pointer-events: auto; } + + .ss-color-summary { + opacity: 1; + transform: translateY(0); + pointer-events: auto; + } } } @@ -122,12 +128,17 @@ background: rgba(255, 255, 255, 0.96); backdrop-filter: blur(8px); box-shadow: 0 6px 18px rgba(12, 19, 31, 0.12); - transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; + // Hidden at rest — revealed only when the card is hovered/focused (like the page-shots), + // so it adds no visual noise to the resting grid. + opacity: 0; + transform: translateY(4px); + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease, box-shadow 0.18s ease, + background 0.18s ease; z-index: 2; gap: 6px; &:hover { - transform: translateY(-1px); background: rgba(255, 255, 255, 0.99); box-shadow: 0 10px 22px rgba(12, 19, 31, 0.16); }