Merge branch 'trunk' into e2e/slack-daily-plugins

This commit is contained in:
Rodel Calasagsag 2023-09-14 15:21:48 +08:00
commit d5a18163da
158 changed files with 5539 additions and 486 deletions

View File

@ -37,6 +37,10 @@ Please take a moment to review the [project readme](https://github.com/woocommer
- Run our build process described in the document on [how to set up WooCommerce development environment](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment), it will install our pre-commit hook, code sniffs, dependencies, and more.
- Before pushing commits to GitHub, check your code against our code standards. For PHP code in the WooCommerce Core project you can do this by running `pnpm --filter=woocommerce run lint:php:changes:branch`.
- Whenever possible, please fix pre-existing code standards errors in code that you change.
- Please consider adding appropriate tests related to your change if applicable such as unit, API and E2E tests. You can check the following guides for this purpose:
- [Writing unit tests](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/README.md#guide-for-writing-unit-tests).
- [Writing API tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests#guide-for-writing-api-tests).
- [Writing E2E tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#guide-for-writing-e2e-tests).
- Ensure you use LF line endings in your code editor. Use [EditorConfig](http://editorconfig.org/) if your editor supports it so that indentation, line endings and other settings are auto configured.
- When committing, reference your issue number (#1234) and include a note about the fix.
- Ensure that your code supports the minimum supported versions of PHP and WordPress; this is shown at the top of the `readme.txt` file.

View File

@ -35,6 +35,7 @@ jobs:
CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }}
CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }}
DEFAULT_TIMEOUT_OVERRIDE: 120000
UPDATE_WC: 'nightly'
steps:
- uses: actions/checkout@v3

View File

@ -1,5 +1,95 @@
== Changelog ==
= 8.1.0 2023-09-12 =
**WooCommerce**
* Fix - Update modified date when a metadata is saved for HPOS. [#39911](https://github.com/woocommerce/woocommerce/pull/39911)
* Fix - Fix edgecase performance issues around incentives caching. [#39958](https://github.com/woocommerce/woocommerce/pull/39958)
* Fix - Add migration to move incorrectly stored payment token IDS to HPOS tables from postmeta. [#39724](https://github.com/woocommerce/woocommerce/pull/39724)
* Fix - Address more PHP 8.1+ deprecation warnings in wc-admin code. [#38774](https://github.com/woocommerce/woocommerce/pull/38774)
* Fix - Adds display of postcodes to Vietnam addresses. [#39403](https://github.com/woocommerce/woocommerce/pull/39403)
* Fix - Always return bool values from WPCacheEngine functions when expected. [#39819](https://github.com/woocommerce/woocommerce/pull/39819)
* Fix - Be more precise when checking submission data from the email verification form on the order confirmation screen. [#39479](https://github.com/woocommerce/woocommerce/pull/39479)
* Fix - Bring HPOS order hooks in line with the posts implementation. [#39694](https://github.com/woocommerce/woocommerce/pull/39694)
* Fix - Connect WC_Install's create_tables to HPOS tables when its active. [#39682](https://github.com/woocommerce/woocommerce/pull/39682)
* Fix - Disable read on sync while backfilling. [#39450](https://github.com/woocommerce/woocommerce/pull/39450)
* Fix - Ensure refund meta data is saved correctly when HPOS is enabled. [#39700](https://github.com/woocommerce/woocommerce/pull/39700)
* Fix - Ensure that the full discount is ignored in free shipping minimum order calculations when ignore_discount setting is enabled [#39155](https://github.com/woocommerce/woocommerce/pull/39155)
* Fix - Fixed a race condition that was causing page views on intro-opt-in page to be sent before tracks was enabled. [#39508](https://github.com/woocommerce/woocommerce/pull/39508)
* Fix - Fixes WooCommerce knowledge base API returning empty posts. [#39809](https://github.com/woocommerce/woocommerce/pull/39809)
* Fix - Fix failure due to multiple h2 tags in the Product Vendors plugin [#38717](https://github.com/woocommerce/woocommerce/pull/38717)
* Fix - Fix Storefront recommendation link and missing image in Marketplace [#39294](https://github.com/woocommerce/woocommerce/pull/39294)
* Fix - include post_ID in HPOS order edit screen [#39321](https://github.com/woocommerce/woocommerce/pull/39321)
* Fix - Limit index length to 191 characters by default, additionally connect HPOS to verify DB tooling. [#39250](https://github.com/woocommerce/woocommerce/pull/39250)
* Fix - Onboarding payments task not completed after setting up WooPayments [#39786](https://github.com/woocommerce/woocommerce/pull/39786)
* Fix - Prevent possible error when refreshing order edit locks. [#39498](https://github.com/woocommerce/woocommerce/pull/39498)
* Fix - Prevent possible fatal error when edit lock is held on deleted order. [#39497](https://github.com/woocommerce/woocommerce/pull/39497)
* Fix - Store transactional data in order tables with HPOS. [#39381](https://github.com/woocommerce/woocommerce/pull/39381)
* Fix - Support inserting NULL values for strict DB mode for DataBase Util's insert_on_duplicate_key_update method. [#39396](https://github.com/woocommerce/woocommerce/pull/39396)
* Fix - Update CSS prop 'end' to 'flex-end' when using flexbox. [#39419](https://github.com/woocommerce/woocommerce/pull/39419)
* Fix - Use admin theme color for select2, instead of hardcoded theme values. [#39451](https://github.com/woocommerce/woocommerce/pull/39451)
* Fix - Use admin theme color instead of old pink. Update old pink to the new brand color. [#39182](https://github.com/woocommerce/woocommerce/pull/39182)
* Fix - [Product Block Editor] remove digital products from the target list #39769 [#39801](https://github.com/woocommerce/woocommerce/pull/39801)
* Add - Add block template registry and controller [#39698](https://github.com/woocommerce/woocommerce/pull/39698)
* Add - Add delete option to generate variations API, to auto delete unmatched variations. [#39733](https://github.com/woocommerce/woocommerce/pull/39733)
* Add - Added feature flag that removes store appearance task and adds customize store task when enabled [#39397](https://github.com/woocommerce/woocommerce/pull/39397)
* Add - Add filter for adding new user preference option for notice to user data fields. [#39685](https://github.com/woocommerce/woocommerce/pull/39685)
* Add - Add plugin installation request track for core profiler [#39533](https://github.com/woocommerce/woocommerce/pull/39533)
* Add - Add post_password for products for REST API V3 [#39438](https://github.com/woocommerce/woocommerce/pull/39438)
* Add - Add support for Japan and UAE in WooPayments [#39431](https://github.com/woocommerce/woocommerce/pull/39431)
* Add - Add woocommerce/product-password-field block to new product editor [#39464](https://github.com/woocommerce/woocommerce/pull/39464)
* Add - API for block-based templates. [#39470](https://github.com/woocommerce/woocommerce/pull/39470)
* Add - Register product catalog and search visibility blocks [#39477](https://github.com/woocommerce/woocommerce/pull/39477)
* Add - Register the product variation items block [#39657](https://github.com/woocommerce/woocommerce/pull/39657)
* Add - [E2E test coverage]: Disable block product editor #39417 [#39493](https://github.com/woocommerce/woocommerce/pull/39493)
* Add - [E2E test coverage]: Enable new product management experience [#39463](https://github.com/woocommerce/woocommerce/pull/39463)
* Add - [E2E test coverage]: General tab #39411 [#39493](https://github.com/woocommerce/woocommerce/pull/39493)
* Update - Add 'variable' to supported post types for product block editor [#39256](https://github.com/woocommerce/woocommerce/pull/39256)
* Update - Added xstate scaffolding for customize your store feature [#39619](https://github.com/woocommerce/woocommerce/pull/39619)
* Update - add time support to product import on sale dates [#39372](https://github.com/woocommerce/woocommerce/pull/39372)
* Update - On the order confirmation screen, show the 'thank you' message regardless of whether the viewer is verified or not. [#39758](https://github.com/woocommerce/woocommerce/pull/39758)
* Update - Support `first_used` and `installation_date` mobile usage data for WCTracker. [#39605](https://github.com/woocommerce/woocommerce/pull/39605)
* Update - Updates Action Scheduler to 3.6.2 (bug fixes and improvements to help debug problems). [#39665](https://github.com/woocommerce/woocommerce/pull/39665)
* Update - Update task list to show a spinner on item click [#39270](https://github.com/woocommerce/woocommerce/pull/39270)
* Update - Update WCPay banners for WooPay in eligible countries. [#39596](https://github.com/woocommerce/woocommerce/pull/39596)
* Update - Update WooCommerce Blocks to 10.9.0 [#39783](https://github.com/woocommerce/woocommerce/pull/39783)
* Update - Update WooCommerce Blocks to 10.9.2 [#39828](https://github.com/woocommerce/woocommerce/pull/39828)
* Update - Use the same checkbox style on the platform selctor [#39469](https://github.com/woocommerce/woocommerce/pull/39469)
* Dev - Added a unit test for plugin feature compatibility data in WC Tracker [#38931](https://github.com/woocommerce/woocommerce/pull/38931)
* Dev - Added storybook for core profiler pages [#39046](https://github.com/woocommerce/woocommerce/pull/39046)
* Dev - Fixed TS type error for state machine Context in Core Profiler that only got caught after TS5 upgrade [#39749](https://github.com/woocommerce/woocommerce/pull/39749)
* Dev - Fixes a failing e2e test in our daily test runs. [#39674](https://github.com/woocommerce/woocommerce/pull/39674)
* Dev - Fix flaky E2E tests in analytics-overview.spec.js. [#39308](https://github.com/woocommerce/woocommerce/pull/39308)
* Dev - Optimized the System Status Report unit tests. [#39363](https://github.com/woocommerce/woocommerce/pull/39363)
* Dev - Refactored some core profiler utils out to reuse them in customise your store. [#39581](https://github.com/woocommerce/woocommerce/pull/39581)
* Dev - Remove dependency on e2e-environment and e2e-utils in wc-admin. [#39746](https://github.com/woocommerce/woocommerce/pull/39746)
* Dev - Remove the non-existing method from TaskList docs. [#39454](https://github.com/woocommerce/woocommerce/pull/39454)
* Dev - Remove unused variation option components [#39673](https://github.com/woocommerce/woocommerce/pull/39673)
* Dev - Runs all API tests on daily run. Skips failing tests on CI. [#39351](https://github.com/woocommerce/woocommerce/pull/39351)
* Dev - Shard the unit tests into two test suites. [#39362](https://github.com/woocommerce/woocommerce/pull/39362)
* Dev - Simplify user id retrieval in analytics-overview.spec.js. [#39472](https://github.com/woocommerce/woocommerce/pull/39472)
* Dev - Update pnpm to 8.6.7 [#39245](https://github.com/woocommerce/woocommerce/pull/39245)
* Dev - Upgrade TypeScript to 5.1.6 [#39531](https://github.com/woocommerce/woocommerce/pull/39531)
* Dev - [Product Block Editor] Disable tabs in parent product page with variations #39459 [#39675](https://github.com/woocommerce/woocommerce/pull/39675)
* Dev - [Product Block Editor] Disable the new editor for variable products. [#39780](https://github.com/woocommerce/woocommerce/pull/39780)
* Tweak - Add loading indicator when submitting location in Tax task [#39613](https://github.com/woocommerce/woocommerce/pull/39613)
* Tweak - Center align checkbox, logo, and the title on the plugins page (core profiler) [#39394](https://github.com/woocommerce/woocommerce/pull/39394)
* Tweak - Do not run 'woocommerce_process_shop_order_meta' for order post when HPOS is authoritative. [#39587](https://github.com/woocommerce/woocommerce/pull/39587)
* Tweak - Fix TikTok naming. [#39748](https://github.com/woocommerce/woocommerce/pull/39748)
* Tweak - Modified the error message shown to customers in the event that no payment gateways are available. [#39348](https://github.com/woocommerce/woocommerce/pull/39348)
* Tweak - Remove subheading letter-spacing from the core profiler pages. [#39526](https://github.com/woocommerce/woocommerce/pull/39526)
* Tweak - Run A/B test on the core profiler plugins page -- suggest Jetpack or Jetpack Boost [#39799](https://github.com/woocommerce/woocommerce/pull/39799)
* Tweak - Safety measures to prevent future breakages when executing bulk actions in the order list table (HPOS). [#39524](https://github.com/woocommerce/woocommerce/pull/39524)
* Tweak - When all plugins are deselected, but Jetpack is already installed and not connected, redirect users to the Jetpack Connect page. [#39109](https://github.com/woocommerce/woocommerce/pull/39109)
* Tweak - When HPOS is authoritative, execute order update logic earlier during the request. [#39590](https://github.com/woocommerce/woocommerce/pull/39590)
* Performance - Use direct meta calls for backfilling instead of expensive object update. [#39450](https://github.com/woocommerce/woocommerce/pull/39450)
* Enhancement - Add filter `woocommerce_pre_delete_{object_type}` which allows preventing deletion.' [#39650](https://github.com/woocommerce/woocommerce/pull/39650)
* Enhancement - Create the Organization tab [#39232](https://github.com/woocommerce/woocommerce/pull/39232)
* Enhancement - Modify order index to also include date for faster order list query. [#39682](https://github.com/woocommerce/woocommerce/pull/39682)
* Enhancement - Update the admin's menu remaining tasks bubble CSS class and handling [#39273](https://github.com/woocommerce/woocommerce/pull/39273)
= 8.0.3 2023-08-29 =
* Update - Bump WooCommerce Blocks to 10.6.6. [#39853](https://github.com/woocommerce/woocommerce/pull/39853)

View File

@ -0,0 +1,5 @@
Significance: patch
Type: fix
Comment: Decode HTML escaped string for tree-item and selected-items components

View File

@ -3,6 +3,7 @@
*/
import classnames from 'classnames';
import { createElement } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
@ -43,7 +44,7 @@ export const SelectedItems = < ItemType, >( {
<div className={ classes }>
{ items
.map( ( item ) => {
return getItemLabel( item );
return decodeEntities( getItemLabel( item ) );
} )
.join( ', ' ) }
</div>

View File

@ -6,6 +6,7 @@ import { __ } from '@wordpress/i18n';
import { chevronDown, chevronUp } from '@wordpress/icons';
import classNames from 'classnames';
import { createElement, forwardRef } from 'react';
import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
@ -72,7 +73,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
{ typeof getLabel === 'function' ? (
getLabel( item )
) : (
<span>{ item.data.label }</span>
<span>{ decodeEntities( item.data.label ) }</span>
) }
</label>

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Fix infinite category loading state

View File

@ -69,7 +69,7 @@ const useTaxonomySearch = (
taxonomyName
);
}
} catch ( e ) {
} finally {
setIsSearching( false );
}
return taxonomies;

View File

@ -1,5 +1,26 @@
# Changelog
## [0.4](https://github.com/woocommerce/woocommerce/releases/tag/0.4) - 2023-09-12
- Patch - Add Woo AI Personalization setting and check setting when generating descriptions with AI.
- Minor - Suggest product categories using AI
- Minor - [Woo AI] Add a Write with AI button for the short description field in product editor.
## [0.3](https://github.com/woocommerce/woocommerce/releases/tag/0.3) - 2023-08-18
- Patch - Fix Woo AI settings page fields persistence bug when disabling the feature.
- Patch - Woo AI - Fix store branding settings retrieval for use with description generation.
- Minor - Adding settings screen for AI centric settings.
- Minor - Generating short description after long description on product editor.
- Minor - [Woo AI] Add Store Branding data to product description generation prompt.
- Minor - Moving text completion hooks into @woocommerce/ai package for reuse.
- Minor - Updating AI endpoints for product editing features.
- Minor - Use additional product data (categories, tags, and attributes) when generating product descriptions.
- Minor - Update pnpm monorepo-wide to 8.6.5
- Minor - Update pnpm to 8.6.7
- Patch - Update `wp-env` to version 8.2.0.
- Minor - Upgrade TypeScript to 5.1.6
## [0.2](https://github.com/woocommerce/woocommerce/releases/tag/0.2) - 2023-06-28
- Minor - Adding error handling for a bad token request.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Adding settings screen for AI centric settings.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
[Woo AI] Add Store Branding data to product description generation prompt.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Generating short description after long description on product editor.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Update pnpm to 8.6.7

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Update pnpm monorepo-wide to 8.6.5

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Update `wp-env` to version 8.2.0.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Upgrade TypeScript to 5.1.6

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Use additional product data (categories, tags, and attributes) when generating product descriptions.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Woo AI - Fix store branding settings retrieval for use with description generation.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fix Woo AI settings page fields persistence bug when disabling the feature.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Add Woo AI Personalization setting and check setting when generating descriptions with AI.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Moving text completion hooks into @woocommerce/ai package for reuse.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
[Woo AI] Add a Write with AI button for the short description field in product editor.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Updating AI endpoints for product editing features.

View File

@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"prefer-stable": true,
"minimum-stability": "dev",
"version": "0.2.0",
"version": "0.4.0",
"require": {
"composer/installers": "~1.7",
"ext-json": "*"

View File

@ -7,7 +7,7 @@
"url": "git://github.com/woocommerce/woo-ai.git"
},
"title": "Woo AI",
"version": "0.2.0",
"version": "0.4.0",
"homepage": "http://github.com/woocommerce/woo-ai",
"devDependencies": {
"@svgr/webpack": "^8.1.0",

View File

@ -0,0 +1 @@
@import 'suggestion-pills/suggestion-pills.scss';

View File

@ -1,3 +1,5 @@
export * from './random-loading-message';
export * from './description-completion-buttons';
export * from './magic-button';
export * from './suggestion-pills';
export * from './info-modal';

View File

@ -0,0 +1 @@
export * from './magic-button';

View File

@ -0,0 +1,31 @@
/**
* Internal dependencies
*/
import MagicIcon from '../../../assets/images/icons/magic.svg';
export type MagicButtonProps = {
title?: string;
disabled?: boolean;
onClick: () => void;
label: string;
};
export const MagicButton = ( {
title,
label,
onClick,
disabled = false,
}: MagicButtonProps ) => {
return (
<button
className="button wp-media-button woo-ai-write-it-for-me-btn"
type="button"
disabled={ disabled }
title={ title }
onClick={ onClick }
>
<img src={ MagicIcon } alt="" />
{ label }
</button>
);
};

View File

@ -131,7 +131,9 @@ export const RandomLoadingMessage: React.FC< RandomLoadingMessageProps > = ( {
<span className="woo-ai-loading-message_spinner">
<Spinner />
</span>
<span>{ currentMessage }</span>
<span className="woo-ai-loading-message_content">
{ currentMessage }
</span>
</>
);
};

View File

@ -0,0 +1 @@
export * from './suggestion-pills';

View File

@ -0,0 +1,25 @@
/**
* External dependencies
*/
import React from 'react';
type SuggestionPillItemProps = {
suggestion: string;
onSuggestionClick: ( suggestion: string ) => void;
};
export const SuggestionPillItem: React.FC< SuggestionPillItemProps > = ( {
suggestion,
onSuggestionClick,
} ) => (
<li className="woo-ai-suggestion-pills__item">
<button
className="button woo-ai-suggestion-pills__select-suggestion"
type="button"
onClick={ () => onSuggestionClick( suggestion ) }
>
<span>+ </span>
<span className="suggestion-content">{ suggestion }</span>
</button>
</li>
);

View File

@ -0,0 +1,21 @@
.woo-ai-suggestion-pills {
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
&__item {
margin-bottom: 0;
}
&__select-suggestion.button {
border-radius: 28px;
min-width: 0;
word-wrap: break-word;
overflow-wrap: anywhere;
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
}
}

View File

@ -0,0 +1,29 @@
/**
* External dependencies
*/
import React from 'react';
/**
* Internal dependencies
*/
import { SuggestionPillItem } from './suggestion-pill-item';
type SuggestionPillsProps = {
suggestions: string[];
onSuggestionClick: ( suggestion: string ) => void;
};
export const SuggestionPills: React.FC< SuggestionPillsProps > = ( {
suggestions,
onSuggestionClick,
} ) => (
<ul className="woo-ai-suggestion-pills">
{ suggestions.map( ( suggestion, index ) => (
<SuggestionPillItem
key={ index }
suggestion={ suggestion }
onSuggestionClick={ () => onSuggestionClick( suggestion ) }
/>
) ) }
</ul>
);

View File

@ -2,3 +2,4 @@ export const WOO_AI_PLUGIN_FEATURE_NAME = 'woo_ai_plugin';
export const MAX_TITLE_LENGTH = 200;
export const MIN_TITLE_LENGTH_FOR_DESCRIPTION = 15;
export const MIN_DESC_LENGTH_FOR_SHORT_DESC = 100;
export const DESCRIPTION_MAX_LENGTH = 300;

View File

@ -9,6 +9,7 @@ import { QueryClient, QueryClientProvider } from 'react-query';
*/
import { WriteItForMeButtonContainer } from './product-description';
import { ProductNameSuggestions } from './product-name';
import { ProductCategorySuggestions } from './product-category';
import { WriteShortDescriptionButtonContainer } from './product-short-description';
import setPreferencesPersistence from './utils/preferencesPersistence';
@ -37,6 +38,16 @@ const renderComponent = ( Component, rootElement ) => {
}
};
const renderProductCategorySuggestions = () => {
const root = document.createElement( 'div' );
root.id = 'woocommerce-ai-app-product-category-suggestions';
renderComponent( ProductCategorySuggestions, root );
// Insert the category suggestions node in the product category meta box.
document.getElementById( 'taxonomy-product_cat' ).append( root );
};
const descriptionButtonRoot = document.getElementById(
'woocommerce-ai-app-product-gpt-button'
);
@ -51,6 +62,7 @@ const shortDescriptionButtonRoot = document.getElementById(
if ( window.JP_CONNECTION_INITIAL_STATE?.connectionStatus?.isActive ) {
renderComponent( WriteItForMeButtonContainer, descriptionButtonRoot );
renderComponent( ProductNameSuggestions, nameSuggestionsRoot );
renderProductCategorySuggestions();
renderComponent(
WriteShortDescriptionButtonContainer,
shortDescriptionButtonRoot

View File

@ -1,3 +1,5 @@
@import 'product-description/product-description.scss';
@import 'product-name/product-name.scss';
@import "components";
@import "product-description/product-description.scss";
@import "product-name/product-name.scss";
@import 'product-category/product-category.scss';
@import 'product-short-description/product-short-description.scss';

View File

@ -0,0 +1,19 @@
.category-suggestions-feedback {
.notice {
position: relative;
span {
vertical-align: middle;
}
.button-group {
.button {
background: initial;
border: none;
}
.button:hover {
text-decoration: underline;
}
}
}
}

View File

@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { recordCategoryTracks } from './utils';
export const CategorySuggestionFeedback = () => {
const [ hide, setHide ] = useState( false );
const submitFeedback = ( positive: boolean ) => {
setHide( true );
recordCategoryTracks( 'feedback', {
response: positive ? 'positive' : 'negative',
} );
};
return (
<div className="category-suggestions-feedback">
{ ! hide && (
<div className="notice notice-info">
<span>{ __( 'How did we do?', 'woocommerce' ) }</span>
<span className="button-group">
<button
type="button"
className="button button-small"
onClick={ () => submitFeedback( true ) }
>
👍
</button>
<button
type="button"
className="button button-small"
onClick={ () => submitFeedback( false ) }
>
👎
</button>
</span>
</div>
) }
</div>
);
};

View File

@ -0,0 +1 @@
export * from './product-category-suggestions';

View File

@ -0,0 +1,372 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useCallback, useEffect, useState } from '@wordpress/element';
import { UseCompletionError } from '@woocommerce/ai';
/**
* Internal dependencies
*/
import { MagicButton, RandomLoadingMessage } from '../components';
import { getCategories, selectCategory } from '../utils';
import AlertIcon from '../../assets/images/icons/alert.svg';
import { getAvailableCategoryPaths, recordCategoryTracks } from './utils';
import { useNewCategorySuggestions } from './useNewCategorySuggestions';
import { useExistingCategorySuggestions } from './useExistingCategorySuggestions';
import { createCategoriesFromPath } from '../utils/categoryCreator';
import { CategorySuggestionFeedback } from './category-suggestion-feedback';
enum SuggestionsState {
Initial,
Fetching,
Failed,
Complete,
None,
}
export const ProductCategorySuggestions = () => {
const [ existingSuggestionsState, setExistingSuggestionsState ] =
useState< SuggestionsState >( SuggestionsState.Initial );
const [ newSuggestionsState, setNewSuggestionsState ] =
useState< SuggestionsState >( SuggestionsState.Initial );
const [ existingSuggestions, setExistingSuggestions ] = useState<
string[]
>( [] );
const [ newSuggestions, setNewSuggestions ] = useState< string[] >( [] );
const [ showFeedback, setShowFeedback ] = useState( false );
let feedbackTimeout: number | null = null;
useEffect( () => {
recordCategoryTracks( 'view_ui' );
}, [] );
/**
* Show the feedback box after a delay.
*/
const showFeedbackAfterDelay = () => {
if ( feedbackTimeout ) {
clearTimeout( feedbackTimeout );
feedbackTimeout = null;
}
feedbackTimeout = setTimeout( () => {
setShowFeedback( true );
}, 5000 );
};
/**
* Reset the feedback box.
*/
const resetFeedbackBox = () => {
if ( feedbackTimeout ) {
clearTimeout( feedbackTimeout );
feedbackTimeout = null;
}
setShowFeedback( false );
};
/**
* Check if a suggestion is valid.
*
* @param suggestion The suggestion to check.
* @param selectedCategories The currently selected categories.
*/
const isSuggestionValid = (
suggestion: string,
selectedCategories: string[]
) => {
return (
suggestion !== __( 'Uncategorized', 'woocommerce' ) &&
! selectedCategories.includes( suggestion )
);
};
/**
* Callback for when the existing category suggestions have been generated.
*
* @param {string[]} existingCategorySuggestions The existing category suggestions.
*/
const onExistingCategorySuggestionsGenerated = async (
existingCategorySuggestions: string[]
) => {
let filtered: string[] = [];
try {
const availableCategories = await getAvailableCategoryPaths();
// Only show suggestions that are valid, available, and not already in the list of new suggestions.
filtered = existingCategorySuggestions.filter(
( suggestion ) =>
isSuggestionValid( suggestion, getCategories() ) &&
availableCategories.includes( suggestion ) &&
! newSuggestions.includes( suggestion )
);
} catch ( e ) {
// eslint-disable-next-line no-console
console.error( 'Unable to fetch available categories.', e );
}
if ( filtered.length === 0 ) {
setExistingSuggestionsState( SuggestionsState.None );
} else {
setExistingSuggestionsState( SuggestionsState.Complete );
}
setExistingSuggestions( filtered );
showFeedbackAfterDelay();
recordCategoryTracks( 'stop', {
reason: 'finished',
suggestions_type: 'existing',
suggestions: existingCategorySuggestions,
valid_suggestions: filtered,
} );
};
/**
* Callback for when the new category suggestions have been generated.
*
* @param {string[]} newCategorySuggestions
*/
const onNewCategorySuggestionsGenerated = async (
newCategorySuggestions: string[]
) => {
let filtered: string[] = [];
try {
const availableCategories = await getAvailableCategoryPaths();
// Only show suggestions that are valid, NOT already available, and not already in the list of existing suggestions.
filtered = newCategorySuggestions.filter(
( suggestion ) =>
isSuggestionValid( suggestion, getCategories() ) &&
! availableCategories.includes( suggestion ) &&
! existingSuggestions.includes( suggestion )
);
} catch ( e ) {
// eslint-disable-next-line no-console
console.error( 'Unable to fetch available categories.', e );
}
if ( filtered.length === 0 ) {
setNewSuggestionsState( SuggestionsState.None );
} else {
setNewSuggestionsState( SuggestionsState.Complete );
}
setNewSuggestions( filtered );
showFeedbackAfterDelay();
recordCategoryTracks( 'stop', {
reason: 'finished',
suggestions_type: 'new',
suggestions: newCategorySuggestions,
valid_suggestions: filtered,
} );
};
/**
* Callback for when an error occurs while generating the existing category suggestions.
*
* @param {UseCompletionError} error
*/
const onExistingCatSuggestionError = ( error: UseCompletionError ) => {
// eslint-disable-next-line no-console
console.debug( 'Streaming error encountered', error );
recordCategoryTracks( 'stop', {
reason: 'error',
suggestions_type: 'existing',
error: error.code ?? error.message,
} );
setExistingSuggestionsState( SuggestionsState.Failed );
};
/**
* Callback for when an error occurs while generating the new category suggestions.
*
* @param {UseCompletionError} error
*/
const onNewCatSuggestionError = ( error: UseCompletionError ) => {
// eslint-disable-next-line no-console
console.debug( 'Streaming error encountered', error );
recordCategoryTracks( 'stop', {
reason: 'error',
suggestions_type: 'new',
error: error.code ?? error.message,
} );
setNewSuggestionsState( SuggestionsState.Failed );
};
const { fetchSuggestions: fetchExistingCategorySuggestions } =
useExistingCategorySuggestions(
onExistingCategorySuggestionsGenerated,
onExistingCatSuggestionError
);
const { fetchSuggestions: fetchNewCategorySuggestions } =
useNewCategorySuggestions(
onNewCategorySuggestionsGenerated,
onNewCatSuggestionError
);
/**
* Callback for when an existing category suggestion is clicked.
*
* @param {string} suggestion The suggestion that was clicked.
*/
const handleExistingSuggestionClick = useCallback(
( suggestion: string ) => {
// remove the selected item from the list of suggestions
setExistingSuggestions(
existingSuggestions.filter( ( s ) => s !== suggestion )
);
selectCategory( suggestion );
recordCategoryTracks( 'select', {
selected_category: suggestion,
suggestions_type: 'existing',
} );
},
[ existingSuggestions ]
);
/**
* Callback for when a new category suggestion is clicked.
*
* @param {string} suggestion The suggestion that was clicked.
*/
const handleNewSuggestionClick = useCallback(
async ( suggestion: string ) => {
// remove the selected item from the list of suggestions
setNewSuggestions(
newSuggestions.filter( ( s ) => s !== suggestion )
);
try {
await createCategoriesFromPath( suggestion );
recordCategoryTracks( 'select', {
selected_category: suggestion,
suggestions_type: 'new',
} );
} catch ( e ) {
// eslint-disable-next-line no-console
console.error( 'Unable to create category', e );
}
},
[ newSuggestions ]
);
const fetchProductSuggestions = async () => {
resetFeedbackBox();
setExistingSuggestions( [] );
setNewSuggestions( [] );
setExistingSuggestionsState( SuggestionsState.Fetching );
setNewSuggestionsState( SuggestionsState.Fetching );
recordCategoryTracks( 'start', {
current_categories: getCategories(),
} );
await Promise.all( [
fetchExistingCategorySuggestions(),
fetchNewCategorySuggestions(),
] );
};
return (
<div className="wc-product-category-suggestions">
<MagicButton
onClick={ fetchProductSuggestions }
disabled={
existingSuggestionsState === SuggestionsState.Fetching ||
newSuggestionsState === SuggestionsState.Fetching
}
label={ __( 'Suggest a category using AI', 'woocommerce' ) }
/>
{ ( existingSuggestionsState === SuggestionsState.Fetching ||
newSuggestionsState === SuggestionsState.Fetching ) && (
<div className="wc-product-category-suggestions__loading notice notice-info">
<p className="wc-product-category-suggestions__loading-message">
<RandomLoadingMessage
isLoading={
existingSuggestionsState ===
SuggestionsState.Fetching ||
newSuggestionsState ===
SuggestionsState.Fetching
}
/>
</p>
</div>
) }
{ existingSuggestionsState === SuggestionsState.None &&
newSuggestionsState === SuggestionsState.None && (
<div className="wc-product-category-suggestions__no-match notice notice-warning">
<p>
{ __(
'Unable to generate a matching category for the product. Please try including more information about the product in the title and description.',
'woocommerce'
) }
</p>
</div>
) }
{ existingSuggestionsState === SuggestionsState.Failed &&
newSuggestionsState === SuggestionsState.Failed && (
<div
className={ `wc-product-category-suggestions__error notice notice-error` }
>
<p className="wc-product-category-suggestions__error-message">
<img src={ AlertIcon } alt="" />
{ __(
`We're currently experiencing high demand for our experimental feature. Please check back in shortly!`,
'woocommerce'
) }
</p>
</div>
) }
{ ( existingSuggestionsState === SuggestionsState.Complete ||
newSuggestionsState === SuggestionsState.Complete ) && (
<div>
<ul className="wc-product-category-suggestions__suggestions">
{ existingSuggestions.map( ( suggestion ) => (
<li key={ suggestion }>
<button
title={ __(
'Select category',
'woocommerce'
) }
className="button-link"
onClick={ () =>
handleExistingSuggestionClick(
suggestion
)
}
>
{ suggestion }
</button>
</li>
) ) }
{ newSuggestions.map( ( suggestion ) => (
<li key={ suggestion }>
<button
title={ __(
'Add and select category',
'woocommerce'
) }
className="button-link"
onClick={ () =>
handleNewSuggestionClick( suggestion )
}
>
{ suggestion }
</button>
</li>
) ) }
</ul>
{ showFeedback && (
<div className="wc-product-category-suggestions__feedback">
<CategorySuggestionFeedback />
</div>
) }
</div>
) }
</div>
);
};

View File

@ -0,0 +1,51 @@
@import "category-suggestion-feedback";
.wc-product-category-suggestions {
&__loading-message {
display: flex;
align-items: center;
.woo-ai-loading-message_spinner {
display: flex;
.woocommerce-spinner {
width: 24px;
height: 24px;
min-width: 24px;
max-height: 24px;
}
}
.woo-ai-loading-message_content {
margin-left: 5px;
}
}
.woo-ai-write-it-for-me-btn {
display: flex;
align-items: center;
img {
filter: invert(32%) sepia(36%) saturate(2913%) hue-rotate(161deg) brightness(87%) contrast(91%);
}
}
&__suggestions {
margin-top: 10px;
margin-bottom: 0;
li:last-child {
margin-bottom: 0;
}
}
.woo-ai-write-it-for-me-btn {
margin: 0;
width: 100%;
display: flex;
justify-content: center;
}
}
.woocommerce-embed-page #wpbody-content .wc-product-category-suggestions .notice {
margin-top: 10px;
margin-bottom: 0;
}

View File

@ -0,0 +1,96 @@
/**
* External dependencies
*/
import {
__experimentalUseCompletion as useCompletion,
UseCompletionError,
} from '@woocommerce/ai';
/**
* Internal dependencies
*/
import { WOO_AI_PLUGIN_FEATURE_NAME } from '../constants';
import { generateProductDataInstructions, ProductProps } from '../utils';
import { getAvailableCategoryPaths } from './utils';
type UseExistingCategorySuggestionsHook = {
fetchSuggestions: () => Promise< void >;
};
export const useExistingCategorySuggestions = (
onSuggestionsGenerated: ( suggestions: string[] ) => void,
onError: ( error: UseCompletionError ) => void
): UseExistingCategorySuggestionsHook => {
const { requestCompletion } = useCompletion( {
feature: WOO_AI_PLUGIN_FEATURE_NAME,
onStreamError: ( error: UseCompletionError ) => {
// eslint-disable-next-line no-console
console.debug( 'Streaming error encountered', error );
onError( error );
},
onCompletionFinished: async ( reason, content ) => {
if ( reason === 'error' ) {
throw Error( 'Invalid response' );
}
if ( ! content ) {
throw Error( 'No suggestions were generated' );
}
try {
const parsed = content
.split( ',' )
.map( ( suggestion ) => {
return suggestion.trim();
} )
.filter( Boolean );
onSuggestionsGenerated( parsed );
} catch ( e ) {
throw Error( 'Unable to parse suggestions' );
}
},
} );
const buildPrompt = async () => {
let availableCategories: string[] = [];
try {
availableCategories = await getAvailableCategoryPaths();
} catch ( e ) {
// eslint-disable-next-line no-console
console.error( 'Unable to fetch available categories', e );
}
const productPropsInstructions = generateProductDataInstructions( {
excludeProps: [ ProductProps.Categories ],
} );
const instructions = [
'You are a WooCommerce SEO and marketing expert.',
`Using the product's ${ productPropsInstructions.includedProps.join(
', '
) } suggest only one category that best matches the product.`,
'Categories can have parents and multi-level children structures like Parent Category > Child Category.',
availableCategories
? `You will be given a list of available categories. Find the best matching category from this list. Available categories are: ${ availableCategories.join(
', '
) }`
: '',
"The product's properties are:",
...productPropsInstructions.instructions,
'Return only one product category, children categories must be separated by >.',
'Here is an example of a valid response:',
'Parent Category > Subcategory > Another Subcategory',
'Do not output the example response. Respond only with the suggested categories. Do not say anything else.',
];
return instructions.join( '\n' );
};
const fetchSuggestions = async () => {
await requestCompletion( await buildPrompt() );
};
return {
fetchSuggestions,
};
};

View File

@ -0,0 +1,82 @@
/**
* External dependencies
*/
import {
__experimentalUseCompletion as useCompletion,
UseCompletionError,
} from '@woocommerce/ai';
/**
* Internal dependencies
*/
import { WOO_AI_PLUGIN_FEATURE_NAME } from '../constants';
import { generateProductDataInstructions, ProductProps } from '../utils';
type UseNewCategorySuggestionsHook = {
fetchSuggestions: () => Promise< void >;
};
export const useNewCategorySuggestions = (
onSuggestionsGenerated: ( suggestions: string[] ) => void,
onError: ( error: UseCompletionError ) => void
): UseNewCategorySuggestionsHook => {
const { requestCompletion } = useCompletion( {
feature: WOO_AI_PLUGIN_FEATURE_NAME,
onStreamError: ( error: UseCompletionError ) => {
// eslint-disable-next-line no-console
console.debug( 'Streaming error encountered', error );
onError( error );
},
onCompletionFinished: async ( reason, content ) => {
if ( reason === 'error' ) {
throw Error( 'Unable to parse suggestions' );
}
if ( ! content ) {
throw Error( 'No suggestions were generated' );
}
try {
const parsed = content
.split( ',' )
.map( ( suggestion ) => {
return suggestion.trim();
} )
.filter( Boolean );
onSuggestionsGenerated( parsed );
} catch ( e ) {
throw Error( 'Unable to parse suggestions' );
}
},
} );
const buildPrompt = async () => {
const productPropsInstructions = generateProductDataInstructions( {
excludeProps: [ ProductProps.Categories ],
} );
const instructions = [
'You are a WooCommerce SEO and marketing expert.',
`Using the product's ${ productPropsInstructions.includedProps.join(
', '
) } suggest the best matching category from the Google standard product category taxonomy hierarchy.`,
'The category can optionally have multi-level children structures like Parent Category > Child Category.',
"The product's properties are:",
...productPropsInstructions.instructions,
'Return only one best matching product category, children categories must be separated by >.',
'Here is an example of a valid response:',
'Parent Category > Child Category > Child of Child Category',
'Do not output the example response. Respond only with the one suggested category. Do not say anything else.',
];
return instructions.join( '\n' );
};
const fetchSuggestions = async () => {
await requestCompletion( await buildPrompt() );
};
return {
fetchSuggestions,
};
};

View File

@ -0,0 +1,84 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { getPostId, recordTracksFactory, decodeHtmlEntities } from '../utils';
type TracksData = Record< string, string | number | null | Array< string > >;
type CategoryProps = {
id: number;
name: string;
parent: number;
};
type CategoriesApiResponse = CategoryProps[];
export const recordCategoryTracks = recordTracksFactory< TracksData >(
'category_completion',
() => ( {
post_id: getPostId(),
} )
);
/**
* Get all available categories in the store.
*
* @return {string[]} Array of categories.
* @throws {Error} If the API request fails.
*/
export const getAvailableCategories =
async (): Promise< CategoriesApiResponse > => {
const results = await apiFetch< CategoriesApiResponse >( {
path: '/wc/v3/products/categories?per_page=100&fields=id,name,parent',
} );
results.forEach( ( category ) => {
category.name = decodeHtmlEntities( category.name );
} );
return results;
};
/**
* Get all available categories in the store as a hierarchical list of strings.
*
* @return {string[]} Array of category names in hierarchical manner where each parent category is separated by a > character. e.g. "Clothing > Shirts > T-Shirts"
* @throws {Error} If the API request fails.
*/
export const getAvailableCategoryPaths = async (): Promise< string[] > => {
const categories: CategoriesApiResponse = await getAvailableCategories();
// Create a map of categories by ID
const categoryNamesById: Record< number, CategoryProps > =
categories.reduce(
( acc, category ) => ( {
...acc,
[ category.id ]: category,
} ),
{}
);
// Get the hierarchy string for each category
return categories.map( ( category ) => {
const hierarchy: string[] = [ category.name ];
let parent = category.parent;
// Traverse up the category hierarchy until the root category is reached
while ( parent !== 0 ) {
const parentCategory = categoryNamesById[ parent ];
if ( parentCategory ) {
hierarchy.push( parentCategory.name );
parent = parentCategory.parent;
} else {
parent = 0;
}
}
// Reverse the hierarchy array so that the parent category is first
return hierarchy.reverse().join( ' > ' );
} );
};

View File

@ -16,6 +16,7 @@ import {
import {
MAX_TITLE_LENGTH,
MIN_TITLE_LENGTH_FOR_DESCRIPTION,
DESCRIPTION_MAX_LENGTH,
WOO_AI_PLUGIN_FEATURE_NAME,
} from '../constants';
import { InfoModal, StopCompletionBtn, WriteItForMeBtn } from '../components';
@ -32,8 +33,6 @@ import { Attribute } from '../utils/types';
import { translateApiErrors as getApiError } from '../utils/apiErrors';
import { buildShortDescriptionPrompt } from '../product-short-description/product-short-description-button-container';
const DESCRIPTION_MAX_LENGTH = 300;
const recordDescriptionTracks = recordTracksFactory(
'description_completion',
() => ( {

View File

@ -0,0 +1,186 @@
/**
* Internal dependencies
*/
import { getAvailableCategories } from '../product-category/utils';
interface HTMLWPListElement extends HTMLElement {
wpList: {
settings: {
addAfter: (
returnedResponse: XMLDocument,
ajaxSettings: object,
wpListSettings: object
) => void;
};
};
}
declare global {
interface Window {
wpAjax: {
parseAjaxResponse: ( response: object ) => {
responses?: {
data?: string;
}[];
};
};
}
}
type NewCategory = {
name: string;
parent_id?: number;
};
/**
* Creates a category in the product category list. This function can only be used where the product category taxonomy list is available (e.g. on the product edit page).
*/
const createCategory = async ( category: NewCategory ) => {
const newCategoryInput = document.getElementById(
'newproduct_cat'
) as HTMLInputElement;
const newCategoryParentSelect = document.getElementById(
'newproduct_cat_parent'
) as HTMLSelectElement;
const newCategoryAddButton = document.getElementById(
'product_cat-add-submit'
) as HTMLButtonElement;
const addCategoryToggle = document.getElementById(
'product_cat-add-toggle'
) as HTMLButtonElement;
const categoryListElement = document.getElementById(
'product_catchecklist'
) as HTMLWPListElement;
if (
! [
newCategoryInput,
newCategoryParentSelect,
newCategoryAddButton,
addCategoryToggle,
categoryListElement,
].every( Boolean )
) {
throw new Error( 'Unable to find the category list elements' );
}
// show and hide the category inputs to make sure they are rendered at least once
addCategoryToggle.click();
addCategoryToggle.click();
// Preserve original addAfter function for restoration after use
const orgCatListAddAfter = categoryListElement.wpList.settings.addAfter;
const categoryCreatedPromise = new Promise< number >( ( resolve ) => {
categoryListElement.wpList.settings.addAfter = ( ...args ) => {
orgCatListAddAfter( ...args );
categoryListElement.wpList.settings.addAfter = orgCatListAddAfter;
const parsedResponse = window.wpAjax.parseAjaxResponse( args[ 0 ] );
if ( ! parsedResponse?.responses?.[ 0 ].data ) {
throw new Error( 'Unable to parse the ajax response' );
}
const parsedHtml = new DOMParser().parseFromString(
parsedResponse.responses[ 0 ].data,
'text/html'
);
const newlyAddedCategoryCheckbox = Array.from(
parsedHtml.querySelectorAll< HTMLInputElement >(
'input[name="tax_input[product_cat][]"]'
)
).find( ( input ) => {
return (
input.parentElement?.textContent?.trim() === category.name
);
} );
if ( ! newlyAddedCategoryCheckbox ) {
throw new Error( 'Unable to find the newly added category' );
}
resolve( Number( newlyAddedCategoryCheckbox.value ) );
};
} );
// Fill category name and select parent category if available
newCategoryInput.value = category.name;
if ( category.parent_id ) {
const parentEl = newCategoryParentSelect.querySelector(
'option[value="' + category.parent_id + '"]'
) as HTMLOptionElement;
if ( ! parentEl ) {
throw new Error( 'Unable to find the parent category in the list' );
}
newCategoryParentSelect.value = category.parent_id.toString();
parentEl.selected = true;
}
// click the add button to create the category
newCategoryAddButton.click();
return categoryCreatedPromise;
};
/**
* Gets the list of categories to create from a given path. The path is a string of categories separated by a > character. e.g. "Clothing > Shirts > T-Shirts"
*
* @param categoryPath
*/
const getCategoriesToCreate = async (
categoryPath: string
): Promise< NewCategory[] > => {
const categories: NewCategory[] = [];
const orderedList = categoryPath.split( ' > ' );
const availableCategories = await getAvailableCategories();
let parentCategoryId = 0;
orderedList.every( ( categoryName, index ) => {
const matchingCategory = availableCategories.find( ( category ) => {
return (
category.name === categoryName &&
category.parent === parentCategoryId
);
} );
if ( matchingCategory ) {
// This is the parent category ID for the next category in the path
parentCategoryId = matchingCategory.id;
} else {
categories.push( {
name: categoryName,
parent_id: parentCategoryId,
} );
for ( let i = index + 1; i < orderedList.length; i++ ) {
categories.push( {
name: orderedList[ i ],
} );
}
return false;
}
return true;
} );
return categories;
};
/**
* Creates categories from a given path. The path is a string of categories separated by a > character. e.g. "Clothing > Shirts > T-Shirts"
*
* @param categoryPath
*/
export const createCategoriesFromPath = async ( categoryPath: string ) => {
const categoriesToCreate = await getCategoriesToCreate( categoryPath );
while ( categoriesToCreate.length ) {
const newCategoryId = await createCategory(
categoriesToCreate.shift() as NewCategory
);
if ( categoriesToCreate.length ) {
// Set the parent ID of the next category in the list to the ID of the newly created category so that it is created as a child of the newly created category
categoriesToCreate[ 0 ].parent_id = newCategoryId;
}
}
};

View File

@ -0,0 +1,72 @@
/**
* Helper function to select a checkbox if it exists within a element
*
* @param element - The DOM element to check for a checkbox
*/
const checkFirstCheckboxInElement = ( element: HTMLElement ) => {
// Select the checkbox input element if it exists
const checkboxElement: HTMLInputElement | null = element.querySelector(
'label > input[type="checkbox"]'
);
// If the checkbox exists, check it and trigger a 'change' event
if ( checkboxElement ) {
checkboxElement.checked = true;
checkboxElement.dispatchEvent( new Event( 'change' ) );
}
};
/**
* Recursive function to select categories and their children based on the provided ordered list
*
* @param orderedCategories - An ordered list of categories to be selected, starting with the top-level category and ending with the lowest-level category.
* @param categoryElements - A list of HTML List elements representing the categories
*/
const selectCategoriesRecursively = (
orderedCategories: string[],
categoryElements: HTMLLIElement[]
) => {
const categoryToSelect = orderedCategories[ 0 ];
// Find the HTML element that matches the category to be selected
const selectedCategoryElement = categoryElements.find(
( element ) =>
element.querySelector( ':scope > label' )?.textContent?.trim() ===
categoryToSelect
);
// If the category to be selected doesn't exist, terminate the function
if ( ! selectedCategoryElement ) {
return;
}
checkFirstCheckboxInElement( selectedCategoryElement );
// Select all the child categories, if they exist
const subsequentCategories: string[] = orderedCategories.slice( 1 );
const childCategoryElements: HTMLLIElement[] = Array.from(
selectedCategoryElement.querySelectorAll( 'ul.children > li' )
);
if ( subsequentCategories.length && childCategoryElements.length ) {
selectCategoriesRecursively(
subsequentCategories,
childCategoryElements
);
}
};
/**
* Main function to select a category and its children from a provided category path
*
* @param categoryPath - The path to the category, each level separated by a > character.
* e.g. "Clothing > Shirts > T-Shirts"
*/
export const selectCategory = ( categoryPath: string ) => {
const categories = categoryPath.split( '>' ).map( ( name ) => name.trim() );
const categoryListElements: HTMLLIElement[] = Array.from(
document.querySelectorAll( '#product_catchecklist > li' )
);
selectCategoriesRecursively( categories, categoryListElements );
};

View File

@ -0,0 +1,7 @@
export const decodeHtmlEntities = ( () => {
const textArea = document.createElement( 'textarea' );
return ( str: string ): string => {
textArea.innerHTML = str;
return textArea.value;
};
} )();

View File

@ -3,3 +3,6 @@ export * from './shuffleArray';
export * from './recordTracksFactory';
export * from './get-post-id';
export * from './tiny-tools';
export * from './productDataInstructionsGenerator';
export * from './categorySelector';
export * from './htmlEntities';

View File

@ -1,21 +1,86 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { Attribute } from './types';
import { getTinyContent } from '.';
export enum ProductProps {
Name = 'name',
Description = 'description',
Categories = 'categories',
Tags = 'tags',
Attributes = 'attributes',
}
/**
* Retrieves a hierarchy string for the specified category element. This includes the category label and all parent categories separated by a > character.
*
* @param {HTMLInputElement} categoryElement - The category element to get the hierarchy string for.
* @return {string} The hierarchy string for the specified category element. e.g. "Clothing > Shirts > T-Shirts"
*/
const getCategoryHierarchy = ( categoryElement: HTMLElement ) => {
let hierarchy = '';
let parentElement = categoryElement.parentElement;
// Traverse up the DOM Tree until a category list item (LI) is found
while ( parentElement ) {
const isListItem = parentElement.tagName.toUpperCase() === 'LI';
const isRootList = parentElement.id === 'product_catchecklist';
if ( isListItem ) {
const categoryLabel =
parentElement.querySelector( 'label' )?.innerText.trim() || '';
if ( categoryLabel ) {
hierarchy = hierarchy
? `${ categoryLabel } > ${ hierarchy }`
: categoryLabel;
} else {
break;
}
}
if ( isRootList ) {
// If the root category list is found, it means we have reached the top of the hierarchy
break;
}
parentElement = parentElement.parentElement;
}
return hierarchy;
};
/**
* Function to get selected categories in hierarchical manner.
*
* @return {string[]} Array of category hierarchy strings for each selected category.
*/
export const getCategories = (): string[] => {
return Array.from(
// Get all the selected category checkboxes
const checkboxes: NodeListOf< HTMLInputElement > =
document.querySelectorAll(
'#taxonomy-product_cat input[name="tax_input[product_cat][]"]'
)
)
.filter(
( item ) =>
window.getComputedStyle( item, ':before' ).content !== 'none'
)
.map( ( item ) => item.nextSibling?.nodeValue?.trim() || '' )
.filter( Boolean );
'#taxonomy-product_cat input[type="checkbox"][name="tax_input[product_cat][]"]'
);
const categoryElements = Array.from( checkboxes );
// Filter out the Uncategorized category and return the remaining selected categories
const selectedCategories = categoryElements.filter( ( element ) => {
const categoryLabel = element.parentElement?.innerText.trim();
return (
element.checked &&
categoryLabel !== __( 'Uncategorized', 'woocommerce' )
);
} );
// Get the hierarchy string for each selected category and filter out any empty strings
return selectedCategories.map( getCategoryHierarchy ).filter( Boolean );
};
const isElementVisible = ( element: HTMLElement ) =>
@ -88,7 +153,7 @@ export const getDescription = (): string => {
const content = document.querySelector(
'#content'
) as HTMLInputElement;
const tinyContent = getTinyContent();
const tinyContent = getTinyContent( 'content', { format: 'text' } );
if ( content && isElementVisible( content ) ) {
return content.value;
} else if ( tinyContent ) {

View File

@ -0,0 +1,114 @@
/**
* Internal dependencies
*/
import { Attribute } from './types';
import {
getAttributes,
getCategories,
getDescription,
getProductName,
getTags,
ProductProps,
} from '.';
import { DESCRIPTION_MAX_LENGTH, MAX_TITLE_LENGTH } from '../constants';
type PropsFilter = {
excludeProps?: ProductProps[];
allowedProps?: ProductProps[];
};
type InstructionSet = {
includedProps: string[];
instructions: string[];
};
/**
* Function to generate prompt instructions for product data.
*
* @param {PropsFilter} propsFilter Object containing the props to be included or excluded from the instructions.
* @param {ProductProps[]} propsFilter.excludeProps Array of props to be excluded from the instructions.
* @param {ProductProps[]} propsFilter.allowedProps Array of props to be included in the instructions.
*
* @return {string[]} Array of prompt instructions.
*/
export const generateProductDataInstructions = ( {
excludeProps,
allowedProps,
}: PropsFilter = {} ): InstructionSet => {
const isPropertyAllowed = ( prop: ProductProps ): boolean => {
if ( allowedProps ) {
return allowedProps.includes( prop );
}
if ( excludeProps ) {
return ! excludeProps.includes( prop );
}
return true;
};
const productName: string = isPropertyAllowed( ProductProps.Name )
? getProductName()
: '';
const productDescription: string = isPropertyAllowed(
ProductProps.Description
)
? getDescription()
: '';
const productCategories: string[] = isPropertyAllowed(
ProductProps.Categories
)
? getCategories()
: [];
const productTags: string[] = isPropertyAllowed( ProductProps.Tags )
? getTags()
: [];
const productAttributes: Attribute[] = isPropertyAllowed(
ProductProps.Attributes
)
? getAttributes()
: [];
const includedProps: string[] = [];
const productPropsInstructions: string[] = [];
if ( productName ) {
productPropsInstructions.push(
`Name: ${ productName.slice( 0, MAX_TITLE_LENGTH ) }.`
);
includedProps.push( 'name' );
}
if ( productDescription ) {
productPropsInstructions.push(
`Description: ${ productDescription.slice(
0,
DESCRIPTION_MAX_LENGTH
) }.`
);
includedProps.push( ProductProps.Description );
}
if ( productCategories.length ) {
productPropsInstructions.push(
`Product categories: ${ productCategories.join( ', ' ) }.`
);
includedProps.push( ProductProps.Categories );
}
if ( productTags.length ) {
productPropsInstructions.push(
`Tagged with: ${ productTags.join( ', ' ) }.`
);
includedProps.push( ProductProps.Tags );
}
if ( productAttributes.length ) {
productAttributes.forEach( ( { name, values } ) => {
productPropsInstructions.push(
`${ name }: ${ values.join( ', ' ) }.`
);
includedProps.push( name );
} );
}
return {
includedProps,
instructions: productPropsInstructions,
};
};

View File

@ -1,5 +1,5 @@
export type TinyContent = {
getContent: () => string;
getContent: ( args?: object ) => string;
setContent: ( str: string ) => void;
id: string;
on: ( event: string, callback: ( event: Event ) => void ) => void;
@ -39,6 +39,6 @@ export const setTinyContent = ( str: string, editorId?: string ) => {
}
};
export const getTinyContent = ( editorId?: string ) => {
return getTinyContentObject( editorId )?.getContent() ?? '';
export const getTinyContent = ( editorId?: string, args?: object ) => {
return getTinyContentObject( editorId )?.getContent( args ) ?? '';
};

View File

@ -3,7 +3,7 @@
* Plugin Name: Woo AI
* Plugin URI: https://github.com/woocommerce/woocommerce/
* Description: Enable AI experiments within the WooCommerce experience. <a href="https://automattic.com/ai-guidelines" target="_blank" rel="noopener noreferrer">Learn more</a>.
* Version: 0.2.0
* Version: 0.4.0
* Author: WooCommerce
* Author URI: http://woocommerce.com/
* Requires at least: 5.8

View File

@ -11,14 +11,23 @@ import { Disabled } from '@wordpress/components';
import {
__unstableEditorStyles as EditorStyles,
__unstableIframe as Iframe,
privateApis as blockEditorPrivateApis,
BlockList,
// @ts-ignore No types for this exist yet.
} from '@wordpress/block-editor';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
import { noop } from 'lodash';
/**
* Internal dependencies
*/
import { LogoBlockContext } from './logo-block-context';
import {
FontFamiliesLoader,
FontFamily,
} from './sidebar/global-styles/font-pairing-variations/font-families-loader';
import { SYSTEM_FONT_SLUG } from './sidebar/global-styles/font-pairing-variations/constants';
const MAX_HEIGHT = 2000;
// @ts-ignore No types for this exist yet.
@ -27,6 +36,8 @@ const { Provider: DisabledProvider } = Disabled.Context;
// This is used to avoid rendering the block list if the sizes change.
let MemoizedBlockList: typeof BlockList | undefined;
const { useGlobalSetting } = unlock( blockEditorPrivateApis );
export type ScaledBlockPreviewProps = {
viewportWidth?: number;
containerWidth: number;
@ -48,6 +59,13 @@ function ScaledBlockPreview( {
onClickNavigationItem,
}: ScaledBlockPreviewProps ) {
const { setLogoBlock } = useContext( LogoBlockContext );
const [ fontFamilies ] = useGlobalSetting(
'typography.fontFamilies.theme'
) as [ FontFamily[] ];
const externalFontFamilies = fontFamilies.filter(
( { slug } ) => slug !== SYSTEM_FONT_SLUG
);
if ( ! viewportWidth ) {
viewportWidth = containerWidth;
@ -254,6 +272,13 @@ function ScaledBlockPreview( {
</style>
{ contentResizeListener }
<MemoizedBlockList renderAppender={ false } />
{ /* Only load font families when there are two font families (font-paring selection). Otherwise, it is not needed. */ }
{ externalFontFamilies.length === 2 && (
<FontFamiliesLoader
fontFamilies={ externalFontFamilies }
onLoad={ noop }
/>
) }
</Iframe>
</DisabledProvider>
);

View File

@ -92,8 +92,13 @@ export const AssemblerHub: CustomizeStoreComponent = ( props ) => {
) => fetchLinkSuggestions( search, searchOptions, settings );
settings.__experimentalFetchRichUrlData = fetchUrlData;
// @ts-ignore No types for this exist yet.
dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters();
const reapplyBlockTypeFilters =
// @ts-ignore No types for this exist yet.
dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters || // GB < 16.6
// @ts-ignore No types for this exist yet.
dispatch( blocksStore ).reapplyBlockTypeFilters; // GB >= 16.6
reapplyBlockTypeFilters();
const coreBlocks = __experimentalGetCoreBlocks().filter(
( { name }: { name: string } ) =>
name !== 'core/freeform' && ! getBlockType( name )

View File

@ -0,0 +1,561 @@
// TODO: Fetch AI-picked color palettes from the backend API
export const COLOR_PALETTES = [
{
title: 'Ancient Bronze',
version: 2,
settings: {
color: {
palette: {
theme: [
{
color: '#11163d',
name: 'Primary',
slug: 'primary',
},
{
color: '#8C8369',
name: 'Secondary',
slug: 'secondary',
},
{
color: '#11163d',
name: 'Foreground',
slug: 'foreground',
},
{
color: '#ffffff',
name: 'Background',
slug: 'background',
},
{
color: '#F7F2EE',
name: 'Tertiary',
slug: 'tertiary',
},
],
},
},
},
styles: {
color: {
background: 'var(--wp--preset--color--background)',
text: 'var(--wp--preset--color--foreground)',
},
elements: {
button: {
color: {
background: 'var(--wp--preset--color--primary)',
text: 'var(--wp--preset--color--background)',
},
},
link: {
color: {
text: 'var(--wp--preset--color--secondary)',
},
':hover': {
color: {
text: 'var(--wp--preset--color--foreground)',
},
},
},
},
},
wpcom_category: 'Neutral',
},
{
title: 'Crimson Tide',
version: 2,
settings: {
color: {
palette: {
theme: [
{
color: '#A02040',
name: 'Primary',
slug: 'primary',
},
{
color: '#234B57',
name: 'Secondary',
slug: 'secondary',
},
{
color: '#871C37',
name: 'Foreground',
slug: 'foreground',
},
{
color: '#ffffff',
name: 'Background',
slug: 'background',
},
{
color: '#FCE5DF',
name: 'Tertiary',
slug: 'tertiary',
},
],
},
},
},
styles: {
color: {
background: 'var(--wp--preset--color--background)',
text: 'var(--wp--preset--color--foreground)',
},
elements: {
button: {
color: {
background: 'var(--wp--preset--color--secondary)',
text: 'var(--wp--preset--color--background)',
},
},
link: {
color: {
text: 'var(--wp--preset--color--secondary)',
},
':hover': {
color: {
text: 'var(--wp--preset--color--foreground)',
},
},
},
},
},
wpcom_category: 'Neutral',
},
{
title: 'Midnight Citrus',
version: 2,
settings: {
color: {
palette: {
theme: [
{
color: '#1B1736',
name: 'Primary',
slug: 'primary',
},
{
color: '#7E76A3',
name: 'Secondary',
slug: 'secondary',
},
{
color: '#1B1736',
name: 'Foreground',
slug: 'foreground',
},
{
color: '#ffffff',
name: 'Background',
slug: 'background',
},
{
color: '#E9FC5F',
name: 'Tertiary',
slug: 'tertiary',
},
],
},
},
},
styles: {
color: {
background: 'var(--wp--preset--color--background)',
text: 'var(--wp--preset--color--foreground)',
},
elements: {
button: {
color: {
background: 'var(--wp--preset--color--primary)',
text: 'var(--wp--preset--color--background)',
},
},
link: {
color: {
text: 'var(--wp--preset--color--foreground)',
},
':hover': {
color: {
text: 'var(--wp--preset--color--primary)',
},
},
},
},
},
wpcom_category: 'Neutral',
},
{
title: 'Fuchsia',
version: 2,
settings: {
color: {
palette: {
theme: [
{
color: '#b7127f',
name: 'Primary',
slug: 'primary',
},
{
color: '#18020C',
name: 'Secondary',
slug: 'secondary',
},
{
color: '#b7127f',
name: 'Foreground',
slug: 'foreground',
},
{
color: '#f7edf6',
name: 'Background',
slug: 'background',
},
{
color: '#ffffff',
name: 'Tertiary',
slug: 'tertiary',
},
],
},
},
},
styles: {
color: {
background: 'var(--wp--preset--color--background)',
text: 'var(--wp--preset--color--foreground)',
},
elements: {
button: {
color: {
background: 'var(--wp--preset--color--foreground)',
text: 'var(--wp--preset--color--background)',
},
},
link: {
color: {
text: 'var(--wp--preset--color--foreground)',
},
':hover': {
color: {
text: 'var(--wp--preset--color--primary)',
},
},
},
},
},
wpcom_category: 'Bright',
},
{
title: 'Raspberry Chocolate',
version: 2,
settings: {
color: {
palette: {
theme: [
{
color: '#42332e',
name: 'Primary',
slug: 'primary',
},
{
color: '#d64d68',
name: 'Secondary',
slug: 'secondary',
},
{
color: '#241d1a',
name: 'Foreground',
slug: 'foreground',
},
{
color: '#eeeae6',
name: 'Background',
slug: 'background',
},
{
color: '#D6CCC2',
name: 'Tertiary',
slug: 'tertiary',
},
],
},
},
},
styles: {
color: {
background: 'var(--wp--preset--color--background)',
text: 'var(--wp--preset--color--foreground)',
},
elements: {
button: {
color: {
background: 'var(--wp--preset--color--primary)',
text: 'var(--wp--preset--color--background)',
},
},
link: {
color: {
text: 'var(--wp--preset--color--secondary)',
},
':hover': {
color: {
text: 'var(--wp--preset--color--primary)',
},
},
},
},
},
wpcom_category: 'Bright',
},
{
title: 'Gumtree Sunset',
version: 2,
settings: {
color: {
palette: {
theme: [
{
color: '#476C77',
name: 'Primary',
slug: 'primary',
},
{
color: '#EFB071',
name: 'Secondary',
slug: 'secondary',
},
{
color: '#476C77',
name: 'Foreground',
slug: 'foreground',
},
{
color: '#edf4f4',
name: 'Background',
slug: 'background',
},
{
color: '#ffffff',
name: 'Tertiary',
slug: 'tertiary',
},
],
},
},
},
styles: {
color: {
background: 'var(--wp--preset--color--background)',
text: 'var(--wp--preset--color--foreground)',
},
elements: {
button: {
color: {
background: 'var(--wp--preset--color--primary)',
text: 'var(--wp--preset--color--tertiary)',
},
},
link: {
color: {
text: 'var(--wp--preset--color--foreground)',
},
':hover': {
color: {
text: 'var(--wp--preset--color--primary)',
},
},
},
},
},
wpcom_category: 'Bright',
},
{
title: 'Ice',
version: 2,
settings: {
color: {
palette: {
theme: [
{
slug: 'primary',
color: '#12123F',
name: 'Primary',
},
{
slug: 'secondary',
color: '#3473FE',
name: 'Secondary',
},
{
slug: 'foreground',
color: '#12123F',
name: 'Foreground',
},
{
slug: 'background',
color: '#F1F4FA',
name: 'Background',
},
{
slug: 'tertiary',
color: '#DBE6EE',
name: 'Tertiary',
},
],
},
},
},
styles: {
color: {
background: 'var(--wp--preset--color--background)',
text: 'var(--wp--preset--color--foreground)',
},
elements: {
button: {
color: {
background: 'var(--wp--preset--color--primary)',
text: 'var(--wp--preset--color--background)',
},
},
link: {
color: {
text: 'var(--wp--preset--color--foreground)',
},
':hover': {
color: {
text: 'var(--wp--preset--color--primary)',
},
},
},
},
},
wpcom_category: 'Bright',
},
{
title: 'Sandalwood Oasis',
version: 2,
settings: {
color: {
palette: {
theme: [
{
color: '#F0EBE3',
name: 'Primary',
slug: 'primary',
},
{
color: '#DF9785',
name: 'Secondary',
slug: 'secondary',
},
{
color: '#ffffff',
name: 'Foreground',
slug: 'foreground',
},
{
color: '#2a2a16',
name: 'Background',
slug: 'background',
},
{
color: '#434323',
name: 'Tertiary',
slug: 'tertiary',
},
],
},
},
},
styles: {
color: {
background: 'var(--wp--preset--color--background)',
text: 'var(--wp--preset--color--foreground)',
},
elements: {
button: {
color: {
background: 'var(--wp--preset--color--primary)',
text: 'var(--wp--preset--color--background)',
},
},
link: {
color: {
text: 'var(--wp--preset--color--secondary)',
},
':hover': {
color: {
text: 'var(--wp--preset--color--primary)',
},
},
},
},
},
wpcom_category: 'Dark',
},
{
title: 'Lilac Nightshade',
version: 2,
settings: {
color: {
palette: {
theme: [
{
color: '#f5d6ff',
name: 'Primary',
slug: 'primary',
},
{
color: '#C48DDA',
name: 'Secondary',
slug: 'secondary',
},
{
color: '#ffffff',
name: 'Foreground',
slug: 'foreground',
},
{
color: '#000000',
name: 'Background',
slug: 'background',
},
{
color: '#462749',
name: 'Tertiary',
slug: 'tertiary',
},
],
},
},
},
styles: {
color: {
background: 'var(--wp--preset--color--background)',
text: 'var(--wp--preset--color--foreground)',
},
elements: {
button: {
color: {
background: 'var(--wp--preset--color--primary)',
text: 'var(--wp--preset--color--background)',
},
},
link: {
color: {
text: 'var(--wp--preset--color--foreground)',
},
':hover': {
color: {
text: 'var(--wp--preset--color--primary)',
},
},
},
},
},
wpcom_category: 'Dark',
},
];

View File

@ -0,0 +1,31 @@
// Reference: https://github.com/WordPress/gutenberg/blob/d5ab7238e53d0947d4bb0853464b1c58325b6130/packages/edit-site/src/components/global-styles/style-variations-container.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
// @ts-ignore No types for this exist yet.
import { __experimentalGrid as Grid } from '@wordpress/components';
/**
* Internal dependencies
*/
import { COLOR_PALETTES } from './constants';
import { VariationContainer } from '../variation-container';
import { ColorPaletteVariationPreview } from './preview';
export const ColorPalette = () => {
return (
<Grid
columns={ 3 }
gap={ 4 }
className="woocommerce-customize-store_color-palette-container"
>
{ COLOR_PALETTES.map( ( variation, index ) => (
<VariationContainer key={ index } variation={ variation }>
<ColorPaletteVariationPreview title={ variation?.title } />
</VariationContainer>
) ) }
</Grid>
);
};

View File

@ -0,0 +1,144 @@
// Reference: https://github.com/Automattic/wp-calypso/blob/d3c9b16fb99ce242f61baa21119b7c20f8823be6/packages/global-styles/src/components/color-palette-variations/preview.tsx#L20
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import {
privateApis as blockEditorPrivateApis,
// @ts-ignore no types exist yet.
} from '@wordpress/block-editor';
import {
// @ts-ignore No types for this exist yet.
__experimentalHStack as HStack,
// @ts-ignore No types for this exist yet.
__experimentalVStack as VStack,
} from '@wordpress/components';
import { useResizeObserver } from '@wordpress/compose';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { GlobalStylesVariationIframe } from '../global-styles-variation-iframe';
export interface Color {
color: string;
name: string;
slug: string;
}
const STYLE_PREVIEW_HEIGHT = 44;
const STYLE_PREVIEW_COLOR_SWATCH_SIZE = 16;
const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorPrivateApis );
interface Props {
title?: string;
}
export const ColorPaletteVariationPreview = ( { title }: Props ) => {
const [ fontWeight ] = useGlobalStyle( 'typography.fontWeight' );
const [ fontFamily = 'serif' ] = useGlobalStyle( 'typography.fontFamily' );
const [ headingFontFamily = fontFamily ] = useGlobalStyle(
'elements.h1.typography.fontFamily'
);
const [ headingFontWeight = fontWeight ] = useGlobalStyle(
'elements.h1.typography.fontWeight'
);
const [ textColor = 'black' ] = useGlobalStyle( 'color.text' );
const [ headingColor = textColor ] = useGlobalStyle(
'elements.h1.color.text'
);
const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' );
const [ gradientValue ] = useGlobalStyle( 'color.gradient' );
const [ themeColors ] = useGlobalSetting( 'color.palette.theme' );
const [ containerResizeListener, { width } ] = useResizeObserver();
const normalizedHeight = STYLE_PREVIEW_HEIGHT;
const normalizedSwatchSize = STYLE_PREVIEW_COLOR_SWATCH_SIZE;
const uniqueColors = [
...new Set< string >(
themeColors.map( ( { color }: Color ) => color )
),
];
const highlightedColors = uniqueColors
.filter(
// we exclude background color because it is already visible in the preview.
( color ) => color !== backgroundColor
)
.slice( 0, 2 );
return (
<GlobalStylesVariationIframe
width={ width }
height={ normalizedHeight }
containerResizeListener={ containerResizeListener }
>
<div
style={ {
// Apply the normalized height only when the width is available
height: width ? normalizedHeight : 0,
width: '100%',
background: gradientValue ?? backgroundColor,
cursor: 'pointer',
} }
>
<div
style={ {
height: '100%',
overflow: 'hidden',
} }
>
{ title ? (
<HStack
spacing={ 1.8875 }
justify="center"
style={ {
height: '100%',
overflow: 'hidden',
} }
>
{ highlightedColors.map( ( color, index ) => (
<div
key={ index }
style={ {
height: normalizedSwatchSize,
width: normalizedSwatchSize,
background: color,
borderRadius: normalizedSwatchSize / 2,
} }
/>
) ) }
</HStack>
) : (
<VStack
spacing={ 3 }
justify="center"
style={ {
height: '100%',
overflow: 'hidden',
padding: 10,
boxSizing: 'border-box',
} }
>
<div
style={ {
fontSize: 40,
fontFamily: headingFontFamily,
color: headingColor,
fontWeight: headingFontWeight,
lineHeight: '1em',
textAlign: 'center',
} }
>
{ __( 'Default', 'woocommerce' ) }
</div>
</VStack>
) }
</div>
</div>
</GlobalStylesVariationIframe>
);
};

View File

@ -0,0 +1,33 @@
// Reference: https://github.com/WordPress/gutenberg/blob/f9e405e0e53d61cd36af4f3b34f2de75874de1e1/packages/edit-site/src/components/global-styles/screen-colors.js#L23
/**
* External dependencies
*/
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
const {
useGlobalStyle,
useGlobalSetting,
useSettingsForBlockElement,
ColorPanel: StylesColorPanel,
} = unlock( blockEditorPrivateApis );
export const ColorPanel = () => {
const [ style ] = useGlobalStyle( '', undefined, 'user', {
shouldDecodeEncode: false,
} );
const [ inheritedStyle, setStyle ] = useGlobalStyle( '', undefined, 'all', {
shouldDecodeEncode: false,
} );
const [ rawSettings ] = useGlobalSetting( '' );
const settings = useSettingsForBlockElement( rawSettings );
return (
<StylesColorPanel
inheritedValue={ inheritedStyle }
value={ style }
onChange={ setStyle }
settings={ settings }
/>
);
};

View File

@ -0,0 +1,503 @@
export const FONT_PREVIEW_LARGE_WIDTH = 136;
export const FONT_PREVIEW_LARGE_HEIGHT = 106;
export const FONT_PREVIEW_WIDTH = 120;
export const FONT_PREVIEW_HEIGHT = 74;
export const SYSTEM_FONT_SLUG = 'system-font';
// Generated from /wpcom/v2/sites/{site_id}/global-styles-variation/font-pairings
// TODO: Consider creating an API endpoint for this data
export const FONT_PAIRINGS = [
{
title: 'Bodoni Moda + Overpass',
version: 2,
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Bodoni Moda',
slug: 'bodoni-moda',
},
{
fontFamily: 'Overpass',
slug: 'overpass',
},
],
},
},
},
styles: {
elements: {
button: {
typography: {
fontFamily: 'var(--wp--preset--font-family--overpass)',
fontWeight: '400',
lineHeight: '1',
},
},
heading: {
typography: {
fontFamily:
'var(--wp--preset--font-family--bodoni-moda)',
fontStyle: 'normal',
fontWeight: '400',
},
},
},
blocks: {
'core/site-title': {
typography: {
fontFamily:
'var(--wp--preset--font-family--bodoni-moda)',
},
},
'core/post-navigation-link': {
typography: {
fontFamily:
'var(--wp--preset--font-family--bodoni-moda)',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--overpass)',
fontSize: 'var(--wp--preset--font-size--medium)',
fontStyle: 'normal',
fontWeight: '300',
lineHeight: '1.6',
},
},
},
{
title: 'Commissioner + Crimson Pro',
version: 2,
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Commissioner',
slug: 'commissioner',
},
{
fontFamily: 'Crimson Pro',
slug: 'crimson-pro',
},
],
},
},
},
styles: {
elements: {
button: {
typography: {
fontFamily:
'var(--wp--preset--font-family--commissioner)',
fontWeight: '400',
lineHeight: '1',
},
},
heading: {
typography: {
fontFamily:
'var(--wp--preset--font-family--commissioner)',
fontStyle: 'normal',
fontWeight: '300',
},
},
},
blocks: {
'core/site-title': {
typography: {
fontFamily:
'var(--wp--preset--font-family--commissioner)',
fontWeight: '300',
},
},
'core/post-navigation-link': {
typography: {
fontFamily:
'var(--wp--preset--font-family--commissioner)',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--crimson-pro)',
fontSize: 'var(--wp--preset--font-size--medium)',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '1.6',
},
},
},
{
title: 'Libre Baskerville + DM Sans',
version: 2,
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Libre Baskerville',
slug: 'libre-baskerville',
},
{
fontFamily: 'DM Sans',
slug: 'dm-sans',
},
],
},
},
},
styles: {
elements: {
button: {
typography: {
fontFamily: 'var(--wp--preset--font-family--dm-sans)',
fontWeight: '400',
lineHeight: '1',
},
},
heading: {
typography: {
fontFamily:
'var(--wp--preset--font-family--libre-baskerville)',
fontStyle: 'normal',
fontWeight: '700',
},
},
},
blocks: {
'core/site-title': {
typography: {
fontFamily:
'var(--wp--preset--font-family--libre-baskerville)',
},
},
'core/post-navigation-link': {
typography: {
fontFamily:
'var(--wp--preset--font-family--libre-baskerville)',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--dm-sans)',
fontSize: 'var(--wp--preset--font-size--small)',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '1.6',
},
},
},
{
title: 'Libre Franklin + EB Garamond',
version: 2,
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Libre Franklin',
slug: 'libre-franklin',
},
{
fontFamily: 'EB Garamond',
slug: 'eb-garamond',
},
],
},
},
},
styles: {
elements: {
button: {
typography: {
fontFamily:
'var(--wp--preset--font-family--libre-franklin)',
fontSize: 'var(--wp--preset--font-size--small)',
fontWeight: '400',
lineHeight: '1',
},
},
heading: {
typography: {
fontFamily:
'var(--wp--preset--font-family--libre-franklin)',
fontStyle: 'normal',
fontWeight: '700',
},
},
},
blocks: {
'core/site-title': {
typography: {
fontFamily:
'var(--wp--preset--font-family--libre-franklin)',
fontWeight: '500',
},
},
'core/post-navigation-link': {
typography: {
fontFamily:
'var(--wp--preset--font-family--libre-franklin)',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--eb-garamond)',
fontSize: 'var(--wp--preset--font-size--medium)',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '1.6',
},
},
},
{
title: 'Montserrat + Arvo',
version: 2,
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Montserrat',
slug: 'montserrat',
},
{
fontFamily: 'Arvo',
slug: 'arvo',
},
],
},
},
},
styles: {
elements: {
button: {
typography: {
fontFamily:
'var(--wp--preset--font-family--montserrat)',
fontStyle: 'normal',
fontWeight: '500',
},
},
heading: {
typography: {
fontFamily:
'var(--wp--preset--font-family--montserrat)',
fontStyle: 'normal',
fontWeight: '700',
lineHeight: '1.4',
},
},
},
blocks: {
'core/site-title': {
typography: {
fontFamily:
'var(--wp--preset--font-family--montserrat)',
fontWeight: '700',
},
},
'core/post-navigation-link': {
typography: {
fontFamily:
'var(--wp--preset--font-family--montserrat)',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--arvo)',
fontSize: 'var(--wp--preset--font-size--small)',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '1.6',
},
},
},
{
title: 'Playfair Display + Fira Sans',
version: 2,
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Playfair Display',
slug: 'playfair-display',
},
{
fontFamily: 'Fira Sans',
slug: 'fira-sans',
},
],
},
},
},
styles: {
elements: {
button: {
typography: {
fontFamily: 'var(--wp--preset--font-family--fira-sans)',
fontWeight: '400',
lineHeight: '1',
},
},
heading: {
typography: {
fontFamily:
'var(--wp--preset--font-family--playfair-display)',
fontStyle: 'italic',
fontWeight: '400',
},
},
},
blocks: {
'core/site-title': {
typography: {
fontFamily:
'var(--wp--preset--font-family--playfair-display)',
fontStyle: 'italic',
fontWeight: '400',
},
},
'core/post-navigation-link': {
typography: {
fontFamily:
'var(--wp--preset--font-family--playfair-display)',
fontStyle: 'italic',
fontWeight: '400',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--fira-sans)',
fontSize: 'var(--wp--preset--font-size--medium)',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '1.6',
},
},
},
{
title: 'Rubik + Inter',
version: 2,
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Rubik',
slug: 'rubik',
},
{
fontFamily: 'Inter',
slug: 'inter',
},
],
},
},
},
styles: {
elements: {
button: {
typography: {
fontFamily: 'var(--wp--preset--font-family--inter)',
fontWeight: '400',
lineHeight: '1',
},
},
heading: {
typography: {
fontFamily: 'var(--wp--preset--font-family--rubik)',
fontStyle: 'normal',
fontWeight: '800',
},
},
},
blocks: {
'core/site-title': {
typography: {
fontFamily: 'var(--wp--preset--font-family--rubik)',
fontWeight: '800',
},
},
'core/post-navigation-link': {
typography: {
fontFamily: 'var(--wp--preset--font-family--rubik)',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--inter)',
fontSize: 'var(--wp--preset--font-size--medium)',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '1.6',
},
},
},
{
title: 'Space Mono + Roboto',
version: 2,
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Space Mono',
slug: 'space-mono',
},
{
fontFamily: 'Roboto',
slug: 'roboto',
},
],
},
},
},
styles: {
elements: {
button: {
typography: {
fontFamily: 'var(--wp--preset--font-family--roboto)',
fontWeight: '400',
lineHeight: '1',
},
},
heading: {
typography: {
fontFamily:
'var(--wp--preset--font-family--space-mono)',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '1.15',
},
},
},
blocks: {
'core/site-title': {
typography: {
fontFamily:
'var(--wp--preset--font-family--space-mono)',
fontStyle: 'normal',
fontWeight: '400',
},
},
'core/post-navigation-link': {
typography: {
fontFamily:
'var(--wp--preset--font-family--space-mono)',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--roboto)',
fontSize: 'var(--wp--preset--font-size--small)',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '1.6',
},
},
},
];

View File

@ -0,0 +1,51 @@
// TODO: We should Download webfonts and host them locally on a site before launching CYS in Core.
// Load font families from wp.com.
/**
* External dependencies
*/
import { useMemo } from '@wordpress/element';
export type FontFamily = {
fontFamily: string;
name: string;
slug: string;
};
type Props = {
fontFamilies: FontFamily[];
onLoad?: () => void;
};
// See https://developers.google.com/fonts/docs/css2
const FONT_API_BASE = 'https://fonts-api.wp.com/css2';
const FONT_AXIS = 'ital,wght@0,400;0,700;1,400;1,700';
export const FontFamiliesLoader = ( { fontFamilies, onLoad }: Props ) => {
const params = useMemo(
() =>
new URLSearchParams( [
...fontFamilies.map( ( { fontFamily } ) => [
'family',
`${ fontFamily }:${ FONT_AXIS }`,
] ),
[ 'display', 'swap' ],
] ),
fontFamilies
);
if ( ! params.getAll( 'family' ).length ) {
return null;
}
return (
<link
rel="stylesheet"
type="text/css"
href={ `${ FONT_API_BASE }?${ params }` }
onLoad={ onLoad }
onError={ onLoad }
/>
);
};

View File

@ -0,0 +1,30 @@
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
// @ts-ignore No types for this exist yet.
import { __experimentalGrid as Grid } from '@wordpress/components';
/**
* Internal dependencies
*/
import { FONT_PAIRINGS } from './constants';
import { VariationContainer } from '../variation-container';
import { FontPairingVariationPreview } from './preview';
export const FontPairing = () => {
return (
<Grid
columns={ 2 }
gap={ 3 }
className="woocommerce-customize-store_font-pairing-container"
>
{ FONT_PAIRINGS.map( ( variation, index ) => (
<VariationContainer key={ index } variation={ variation }>
<FontPairingVariationPreview />
</VariationContainer>
) ) }
</Grid>
);
};

View File

@ -0,0 +1,176 @@
// Reference: https://github.com/Automattic/wp-calypso/blob/d3c9b16fb99ce242f61baa21119b7c20f8823be6/packages/global-styles/src/components/font-pairing-variations/preview.tsx
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import {
// @ts-ignore No types for this exist yet.
__experimentalHStack as HStack,
// @ts-ignore No types for this exist yet.
__experimentalVStack as VStack,
} from '@wordpress/components';
import { useResizeObserver, useViewportMatch } from '@wordpress/compose';
import { useMemo, useState } from '@wordpress/element';
import {
privateApis as blockEditorPrivateApis,
// @ts-ignore no types exist yet.
} from '@wordpress/block-editor';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
import { GlobalStylesVariationIframe } from '../global-styles-variation-iframe';
import { FontFamiliesLoader, FontFamily } from './font-families-loader';
import {
FONT_PREVIEW_LARGE_WIDTH,
FONT_PREVIEW_LARGE_HEIGHT,
FONT_PREVIEW_WIDTH,
FONT_PREVIEW_HEIGHT,
SYSTEM_FONT_SLUG,
} from './constants';
const { useGlobalStyle, useGlobalSetting } = unlock( blockEditorPrivateApis );
const DEFAULT_LARGE_FONT_STYLES: React.CSSProperties = {
fontSize: '13vw', // 18px for min-width wide breakpoint and 15px for max-width wide
lineHeight: '20px',
color: '#000000',
};
export const FontPairingVariationPreview = () => {
const [ fontFamilies ] = useGlobalSetting(
'typography.fontFamilies.theme'
) as [ FontFamily[] ];
const [ textFontFamily = 'serif' ] = useGlobalStyle(
'typography.fontFamily'
);
const [ textFontStyle = 'normal' ] = useGlobalStyle(
'typography.fontStyle'
);
const [ textLetterSpacing = '-0.15px' ] = useGlobalStyle(
'typography.letterSpacing'
);
const [ textFontWeight = 400 ] = useGlobalStyle( 'typography.fontWeight' );
const [ headingFontFamily = textFontFamily ] = useGlobalStyle(
'elements.heading.typography.fontFamily'
);
const [ headingFontStyle = textFontStyle ] = useGlobalStyle(
'elements.heading.typography.fontStyle'
);
const [ headingFontWeight = textFontWeight ] = useGlobalStyle(
'elements.heading.typography.fontWeight'
);
const [ headingLetterSpacing = textLetterSpacing ] = useGlobalStyle(
'elements.heading.typography.letterSpacing'
);
const [ containerResizeListener, { width } ] = useResizeObserver();
const isDesktop = useViewportMatch( 'large' );
const defaultWidth = isDesktop
? FONT_PREVIEW_LARGE_WIDTH
: FONT_PREVIEW_WIDTH;
const defaultHeight = isDesktop
? FONT_PREVIEW_LARGE_HEIGHT
: FONT_PREVIEW_HEIGHT;
const ratio = width ? width / defaultWidth : 1;
const normalizedHeight = Math.ceil( defaultHeight * ratio );
const externalFontFamilies = fontFamilies.filter(
( { slug } ) => slug !== SYSTEM_FONT_SLUG
);
const [ isLoaded, setIsLoaded ] = useState( ! externalFontFamilies.length );
const getFontFamilyName = ( targetFontFamily: string ) => {
const fontFamily = fontFamilies.find(
( { fontFamily: _fontFamily } ) => _fontFamily === targetFontFamily
);
return fontFamily?.name || fontFamily?.fontFamily || targetFontFamily;
};
const textFontFamilyName = useMemo(
() => getFontFamilyName( textFontFamily ),
[ textFontFamily, fontFamilies ]
);
const headingFontFamilyName = useMemo(
() => getFontFamilyName( headingFontFamily ),
[ headingFontFamily, fontFamilies ]
);
const handleOnLoad = () => setIsLoaded( true );
return (
<GlobalStylesVariationIframe
width={ width }
height={ normalizedHeight }
containerResizeListener={ containerResizeListener }
>
<>
<div
style={ {
// Apply the normalized height only when the width is available
height: width ? normalizedHeight : 0,
width: '100%',
background: 'white',
cursor: 'pointer',
} }
>
<div
style={ {
height: '100%',
overflow: 'hidden',
opacity: isLoaded ? 1 : 0,
} }
>
<HStack
spacing={ 10 * ratio }
justify="flex-start"
style={ {
height: '100%',
overflow: 'hidden',
} }
>
<VStack
spacing={ 1 }
style={ {
margin: '10px',
width: '100%',
textAlign: isDesktop ? 'center' : 'left',
} }
>
<div
aria-label={ headingFontFamilyName }
style={ {
...DEFAULT_LARGE_FONT_STYLES,
letterSpacing: headingLetterSpacing,
fontWeight: headingFontWeight,
fontFamily: headingFontFamily,
fontStyle: headingFontStyle,
} }
>
{ headingFontFamilyName }
</div>
<div
aria-label={ textFontFamilyName }
style={ {
...DEFAULT_LARGE_FONT_STYLES,
fontSize: '13px',
letterSpacing: textLetterSpacing,
fontWeight: textFontWeight,
fontFamily: textFontFamily,
fontStyle: textFontStyle,
} }
>
{ textFontFamilyName }
</div>
</VStack>
</HStack>
</div>
</div>
<FontFamiliesLoader
fontFamilies={ externalFontFamilies }
onLoad={ handleOnLoad }
/>
</>
</GlobalStylesVariationIframe>
);
};

View File

@ -0,0 +1,98 @@
// Reference: https://github.com/Automattic/wp-calypso/blob/d3c9b16fb99ce242f61baa21119b7c20f8823be6/packages/global-styles/src/components/global-styles-variation-container/index.tsx#L19
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import {
__unstableIframe as Iframe,
__unstableEditorStyles as EditorStyles,
privateApis as blockEditorPrivateApis,
// @ts-ignore no types exist yet.
} from '@wordpress/block-editor';
import { useRefEffect } from '@wordpress/compose';
import { useMemo } from 'react';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
/**
* Internal dependencies
*/
import './style.scss';
const { useGlobalStylesOutput } = unlock( blockEditorPrivateApis );
interface Props {
width: number | null;
height: number;
inlineCss?: string;
containerResizeListener: JSX.Element;
children: JSX.Element;
onFocusOut?: () => void;
}
export const GlobalStylesVariationIframe = ( {
width,
height,
inlineCss,
containerResizeListener,
children,
onFocusOut,
...props
}: Props ) => {
const [ styles ] = useGlobalStylesOutput();
// Reset leaked styles from WP common.css and remove main content layout padding and border.
const editorStyles = useMemo( () => {
if ( styles ) {
return [
...styles,
...( inlineCss
? [
{
css: inlineCss,
isGlobalStyles: true,
},
]
: [] ),
{
css: 'html{overflow:hidden}body{min-width: 0;padding: 0;border: none;transform:scale(1);}',
isGlobalStyles: true,
},
];
}
return styles;
}, [ inlineCss, styles ] );
return (
<Iframe
className="global-styles-variation-container__iframe"
style={ {
height,
visibility: width ? 'visible' : 'hidden',
} }
tabIndex={ -1 }
contentRef={ useRefEffect( ( bodyElement ) => {
// Disable moving focus to the writing flow wrapper if the focus disappears
// See https://github.com/WordPress/gutenberg/blob/aa8e1c52c7cb497e224a479673e584baaca97113/packages/block-editor/src/components/writing-flow/use-tab-nav.js#L136
const handleFocusOut = ( event: Event ) => {
event.stopImmediatePropagation();
// Explicitly call the focusOut handler, if available.
onFocusOut?.();
};
bodyElement.addEventListener( 'focusout', handleFocusOut );
return () => {
bodyElement.removeEventListener(
'focusout',
handleFocusOut
);
};
}, [] ) }
scrolling="no"
{ ...props }
>
<EditorStyles styles={ editorStyles ?? [] } />
{ containerResizeListener }
{ children }
</Iframe>
);
};

View File

@ -0,0 +1,82 @@
.global-styles-variation-container__iframe {
border-radius: 3px; /* stylelint-disable-line scales/radii */
box-shadow: 0 0 0 1px rgb(0 0 0 / 10%);
border: 0;
display: block;
max-width: 100%;
}
.color-block-support-panel {
padding: 0;
border-top: 0;
.components-tools-panel-header {
display: none;
}
.block-editor-tools-panel-color-gradient-settings__dropdown {
.components-button {
color: $gray-900;
font-size: 0.8125rem;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 123.077% */
&.is-open {
background: initial;
color: $gray-900;
}
&:focus {
box-shadow: none;
}
}
}
.block-editor-panel-color-gradient-settings__dropdown {
padding: 12px 0;
.components-flex {
flex-direction: row-reverse;
justify-content: space-between;
}
}
.color-block-support-panel__inner-wrapper {
.block-editor-tools-panel-color-gradient-settings__item {
border: 0;
order: 10; // default
// Background
&:nth-child(2) {
order: 1;
}
// Text
&.first {
border-top: 0;
order: 2;
}
// Heading
&:nth-child(6) {
order: 3;
}
// Button
&:nth-child(5) {
order: 4;
}
// Link
&:nth-child(3) {
order: 5;
}
}
}
.block-editor-color-gradient-control__panel {
> .components-flex > .components-h-stack.components-v-stack {
display: none;
}
}
}

View File

@ -0,0 +1,3 @@
export { ColorPalette } from './color-palette-variations';
export { ColorPanel } from './color-panel';
export { FontPairing } from './font-pairing-variations';

View File

@ -0,0 +1,94 @@
// Reference: https://github.com/WordPress/gutenberg/blob/d5ab7238e53d0947d4bb0853464b1c58325b6130/packages/edit-site/src/components/global-styles/style-variations-container.js
/**
* External dependencies
*/
import classnames from 'classnames';
import { useMemo, useContext } from '@wordpress/element';
import { ENTER } from '@wordpress/keycodes';
import { __, sprintf } from '@wordpress/i18n';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/components/global-styles/global-styles-provider';
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
import { isEqual } from 'lodash';
const { GlobalStylesContext } = unlock( blockEditorPrivateApis );
export const VariationContainer = ( { variation, children } ) => {
const { base, user, setUserConfig } = useContext( GlobalStylesContext );
const context = useMemo( () => {
return {
user: {
settings: variation.settings ?? {},
styles: variation.styles ?? {},
},
base,
merged: mergeBaseAndUserConfigs( base, variation ),
setUserConfig: () => {},
};
}, [ variation, base ] );
const selectVariation = () => {
setUserConfig( () => {
return {
settings: mergeBaseAndUserConfigs(
user.settings,
variation.settings
),
styles: mergeBaseAndUserConfigs(
user.styles,
variation.styles
),
};
} );
};
const selectOnEnter = ( event ) => {
if ( event.keyCode === ENTER ) {
event.preventDefault();
selectVariation();
}
};
const isActive = useMemo( () => {
if ( variation.settings.color ) {
return isEqual( variation.settings.color, user.settings.color );
}
return isEqual(
variation.settings.typography,
user.settings.typography
);
}, [ user, variation ] );
let label = variation?.title;
if ( variation?.description ) {
label = sprintf(
/* translators: %1$s: variation title. %2$s variation description. */
__( '%1$s (%2$s)', 'woocommerce' ),
variation?.title,
variation?.description
);
}
return (
<GlobalStylesContext.Provider value={ context }>
<div
className={ classnames(
'woocommerce-customize-store_global-styles-variations_item',
{
'is-active': isActive,
}
) }
role="button"
onClick={ selectVariation }
onKeyDown={ selectOnEnter }
tabIndex="0"
aria-label={ label }
aria-current={ isActive }
>
<div className="woocommerce-customize-store_global-styles-variations_item-preview">
{ children }
</div>
</div>
</GlobalStylesContext.Provider>
);
};

View File

@ -13,10 +13,6 @@ import {
// @ts-ignore No types for this exist yet.
__experimentalUseNavigator as useNavigator,
} from '@wordpress/components';
// @ts-ignore No types for this exist yet.
import { privateApis as routerPrivateApis } from '@wordpress/router';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
/**
* Internal dependencies
@ -31,8 +27,12 @@ import { SidebarNavigationScreenPages } from './sidebar-navigation-screen-pages'
import { SidebarNavigationScreenLogo } from './sidebar-navigation-screen-logo';
import { SaveHub } from './save-hub';
const { useLocation, useHistory } = unlock( routerPrivateApis );
import {
addHistoryListener,
getQuery,
updateQueryString,
useQuery,
} from '@woocommerce/navigation';
function isSubset(
subset: {
@ -48,18 +48,27 @@ function isSubset(
}
function useSyncPathWithURL() {
const history = useHistory();
const { params: urlParams } = useLocation();
const { location: navigatorLocation, params: navigatorParams } =
useNavigator();
const urlParams = useQuery();
const {
location: navigatorLocation,
params: navigatorParams,
goTo,
} = useNavigator();
const isMounting = useRef( true );
useEffect(
() => {
// The navigatorParams are only initially filled properly when the
// navigator screens mount. so we ignore the first synchronisation.
// The navigatorParams are only initially filled properly after the
// navigator screens mounts. so we don't do the query string update initially.
// however we also do want to add an event listener for popstate so that we can
// update the navigator when the user navigates using the browser back button
if ( isMounting.current ) {
isMounting.current = false;
addHistoryListener( ( event: PopStateEvent ) => {
if ( event.type === 'popstate' ) {
goTo( ( getQuery() as Record< string, string > ).path );
}
} );
return;
}
@ -73,7 +82,7 @@ function useSyncPathWithURL() {
...urlParams,
...newUrlParams,
};
history.push( updatedParams );
updateQueryString( {}, updatedParams.path );
}
updateUrlParams( {
@ -97,28 +106,28 @@ function SidebarScreens() {
useSyncPathWithURL();
return (
<>
<NavigatorScreen path="/customize-store">
<NavigatorScreen path="/customize-store/assembler-hub">
<SidebarNavigationScreenMain />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/color-palette">
<NavigatorScreen path="/customize-store/assembler-hub/color-palette">
<SidebarNavigationScreenColorPalette />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/typography">
<NavigatorScreen path="/customize-store/assembler-hub/typography">
<SidebarNavigationScreenTypography />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/header">
<NavigatorScreen path="/customize-store/assembler-hub/header">
<SidebarNavigationScreenHeader />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/homepage">
<NavigatorScreen path="/customize-store/assembler-hub/homepage">
<SidebarNavigationScreenHomepage />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/footer">
<NavigatorScreen path="/customize-store/assembler-hub/footer">
<SidebarNavigationScreenFooter />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/pages">
<NavigatorScreen path="/customize-store/assembler-hub/pages">
<SidebarNavigationScreenPages />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/logo">
<NavigatorScreen path="/customize-store/assembler-hub/logo">
<SidebarNavigationScreenLogo />
</NavigatorScreen>
</>
@ -126,8 +135,10 @@ function SidebarScreens() {
}
function Sidebar() {
const { params: urlParams } = useLocation();
const initialPath = useRef( urlParams.path ?? '/customize-store' );
const urlParams = getQuery() as Record< string, string >;
const initialPath = useRef(
urlParams.path ?? '/customize-store/assembler-hub'
);
return (
<>
<NavigatorProvider

View File

@ -5,7 +5,7 @@
* External dependencies
*/
import { useContext, useEffect } from '@wordpress/element';
import { useQuery } from '@woocommerce/navigation';
import { useSelect, useDispatch } from '@wordpress/data';
// @ts-ignore No types for this exist yet.
import { Button, __experimentalHStack as HStack } from '@wordpress/components';
@ -14,14 +14,9 @@ import { __ } from '@wordpress/i18n';
import { store as coreStore } from '@wordpress/core-data';
// @ts-ignore No types for this exist yet.
import { store as blockEditorStore } from '@wordpress/block-editor';
// @ts-ignore No types for this exist yet.
import { privateApis as routerPrivateApis } from '@wordpress/router';
// @ts-ignore No types for this exist yet.
import { store as noticesStore } from '@wordpress/notices';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import { useEntitiesSavedStatesIsDirty as useIsDirty } from '@wordpress/editor';
/**
@ -29,8 +24,6 @@ import { useEntitiesSavedStatesIsDirty as useIsDirty } from '@wordpress/editor';
*/
import { CustomizeStoreContext } from '../';
const { useLocation } = unlock( routerPrivateApis );
const PUBLISH_ON_SAVE_ENTITIES = [
{
kind: 'postType',
@ -40,7 +33,7 @@ const PUBLISH_ON_SAVE_ENTITIES = [
export const SaveHub = () => {
const saveNoticeId = 'site-edit-save-notice';
const { params } = useLocation();
const urlParams = useQuery();
const { sendEvent } = useContext( CustomizeStoreContext );
// @ts-ignore No types for this exist yet.
@ -111,6 +104,20 @@ export const SaveHub = () => {
},
{ undoIgnore: true }
);
} else if (
entity.kind === 'root' &&
entity.name === 'globalStyles'
) {
editEntityRecord(
entity.kind,
entity.name,
entity.key,
{
styles: undefined,
settings: undefined,
},
{ undoIgnore: true }
);
} else {
editEntityRecord(
entity.kind,
@ -127,7 +134,7 @@ export const SaveHub = () => {
} );
// Only run when path changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ params.path ] );
}, [ urlParams.path ] );
const save = async () => {
removeNotice( saveNoticeId );
@ -168,7 +175,7 @@ export const SaveHub = () => {
};
const renderButton = () => {
if ( params.path === '/customize-store' ) {
if ( urlParams.path === '/customize-store/assembler-hub' ) {
return (
<Button
variant="primary"

View File

@ -1,15 +1,62 @@
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import { Link } from '@woocommerce/components';
import { useSelect } from '@wordpress/data';
// @ts-ignore no types exist yet.
import { BlockEditorProvider } from '@wordpress/block-editor';
import { noop } from 'lodash';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
import { PanelBody } from '@wordpress/components';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
import { ColorPalette, ColorPanel } from './global-styles';
const SidebarNavigationScreenColorPaletteContent = () => {
const { storedSettings } = useSelect( ( select ) => {
const { getSettings } = unlock( select( editSiteStore ) );
return {
storedSettings: getSettings( false ),
};
}, [] );
// Wrap in a BlockEditorProvider to ensure that the Iframe's dependencies are
// loaded. This is necessary because the Iframe component waits until
// the block editor store's `__internalIsInitialized` is true before
// rendering the iframe. Without this, the iframe previews will not render
// in mobile viewport sizes, where the editor canvas is hidden.
return (
<div className="woocommerce-customize-store_sidebar-color-content">
<BlockEditorProvider
settings={ storedSettings }
onChange={ noop }
onInput={ noop }
>
<ColorPalette />
<PanelBody
className="woocommerce-customize-store__color-panel-container"
title={ __( 'or create your own', 'woocommerce' ) }
initialOpen={ false }
>
<ColorPanel />
</PanelBody>
</BlockEditorProvider>
</div>
);
};
export const SidebarNavigationScreenColorPalette = () => {
return (
@ -23,23 +70,19 @@ export const SidebarNavigationScreenColorPalette = () => {
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
href={ `${ ADMIN_URL }site-editor.php` }
type="external"
/>
),
StyleLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php?path=%2Fwp_global_styles&canvas=edit` }
href={ `${ ADMIN_URL }site-editor.php?path=%2Fwp_global_styles&canvas=edit` }
type="external"
/>
),
}
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
</>
}
content={ <SidebarNavigationScreenColorPaletteContent /> }
/>
);
};

View File

@ -22,7 +22,7 @@ export const SidebarNavigationScreenFooter = () => {
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
href={ `${ ADMIN_URL }site-editor.php` }
type="external"
/>
),

View File

@ -22,7 +22,7 @@ export const SidebarNavigationScreenHeader = () => {
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
href={ `${ ADMIN_URL }site-editor.php` }
type="external"
/>
),

View File

@ -22,7 +22,7 @@ export const SidebarNavigationScreenHomepage = () => {
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
href={ `${ ADMIN_URL }site-editor.php` }
type="external"
/>
),

View File

@ -45,7 +45,7 @@ export const SidebarNavigationScreenMain = () => {
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
href={ `${ ADMIN_URL }site-editor.php` }
type="external"
/>
),
@ -61,7 +61,7 @@ export const SidebarNavigationScreenMain = () => {
<ItemGroup>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/logo"
path="/customize-store/assembler-hub/logo"
withChevron
icon={ siteLogo }
>
@ -69,7 +69,7 @@ export const SidebarNavigationScreenMain = () => {
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/color-palette"
path="/customize-store/assembler-hub/color-palette"
withChevron
icon={ color }
>
@ -77,7 +77,7 @@ export const SidebarNavigationScreenMain = () => {
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/typography"
path="/customize-store/assembler-hub/typography"
withChevron
icon={ typography }
>
@ -92,7 +92,7 @@ export const SidebarNavigationScreenMain = () => {
<ItemGroup>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/header"
path="/customize-store/assembler-hub/header"
withChevron
icon={ header }
>
@ -100,7 +100,7 @@ export const SidebarNavigationScreenMain = () => {
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/homepage"
path="/customize-store/assembler-hub/homepage"
withChevron
icon={ home }
>
@ -108,7 +108,7 @@ export const SidebarNavigationScreenMain = () => {
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/footer"
path="/customize-store/assembler-hub/footer"
withChevron
icon={ footer }
>
@ -116,7 +116,7 @@ export const SidebarNavigationScreenMain = () => {
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/pages"
path="/customize-store/assembler-hub/pages"
withChevron
icon={ pages }
>

View File

@ -23,13 +23,13 @@ export const SidebarNavigationScreenPages = () => {
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
href={ `${ ADMIN_URL }site-editor.php` }
type="external"
/>
),
PageLink: (
<Link
href={ `${ ADMIN_URL }/edit.php?post_type=page` }
href={ `${ ADMIN_URL }edit.php?post_type=page` }
type="external"
/>
),

View File

@ -1,17 +1,36 @@
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import { Link } from '@woocommerce/components';
import { useSelect } from '@wordpress/data';
// @ts-ignore no types exist yet.
import { BlockEditorProvider } from '@wordpress/block-editor';
import { noop } from 'lodash';
// @ts-ignore No types for this exist yet.
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
import { FontPairing } from './global-styles';
export const SidebarNavigationScreenTypography = () => {
const { storedSettings } = useSelect( ( select ) => {
const { getSettings } = unlock( select( editSiteStore ) );
return {
storedSettings: getSettings( false ),
};
}, [] );
return (
<SidebarNavigationScreen
title={ __( 'Change your font', 'woocommerce' ) }
@ -23,22 +42,28 @@ export const SidebarNavigationScreenTypography = () => {
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
href={ `${ ADMIN_URL }site-editor.php` }
type="external"
/>
),
StyleLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php?path=%2Fwp_global_styles&canvas=edit` }
href={ `${ ADMIN_URL }site-editor.php?path=%2Fwp_global_styles&canvas=edit` }
type="external"
/>
),
}
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
</>
<div className="woocommerce-customize-store_sidebar-typography-content">
<BlockEditorProvider
settings={ storedSettings }
onChange={ noop }
onInput={ noop }
>
<FontPairing />
</BlockEditorProvider>
</div>
}
/>
);

View File

@ -113,12 +113,16 @@
/* Sidebar */
.edit-site-layout__sidebar-region {
width: 380px;
z-index: 3;
}
.edit-site-layout__sidebar {
.edit-site-sidebar__content > div {
padding: 0 16px;
overflow-x: hidden;
.edit-site-sidebar__content {
.components-navigator-screen {
will-change: auto;
padding: 0 16px;
overflow-x: hidden;
}
}
.edit-site-sidebar-button {
@ -194,7 +198,11 @@
.edit-site-save-hub {
border-top: 0;
padding: 32px 29px 32px 35px;
padding: 32px 29px 32px 32px;
button.is-primary:disabled {
opacity: 0.5;
}
}
}
@ -313,6 +321,80 @@
}
}
/* Color sidebar */
.woocommerce-customize-store__color-panel-container {
border: 0;
margin-top: 24px;
padding: 0;
.components-panel__body-title {
margin: 0 !important;
&:hover {
background: initial;
}
}
.components-panel__body-toggle.components-button {
padding: 16px 0;
text-transform: uppercase;
color: $gray-900;
font-size: 0.6875rem;
font-weight: 600;
line-height: 16px; /* 145.455% */
&:focus {
box-shadow: none;
}
.components-panel__arrow {
right: 0;
color: $gray-900;
}
}
}
.woocommerce-customize-store_sidebar-color-content {
width: 324px;
.woocommerce-customize-store_global-styles-variations_item {
border-radius: 2px;
padding: 2.5px;
.woocommerce-customize-store_global-styles-variations_item-preview {
border: 1px solid #dcdcde;
background: #fff;
}
&:hover,
&.is-active {
box-shadow: 0 0 0 1.5px var(--wp-admin-theme-color), 0 0 0 2.5px #fff;
}
}
}
.woocommerce-customize-store_sidebar-typography-content {
width: 324px;
.woocommerce-customize-store_global-styles-variations_item {
border-radius: 2px;
border: 1px solid #dcdcde;
background: #fff;
&:hover,
&.is-active {
border-radius: 2px;
border: 1.5px solid var(--wp-admin-theme-color);
background: #fff;
}
}
.global-styles-variation-container__iframe {
box-shadow: none;
}
}
/* Preview Canvas */
.edit-site-layout__canvas {
bottom: 16px;

View File

@ -0,0 +1,43 @@
<svg width="216" height="136" viewBox="0 0 216 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Layer_1" clip-path="url(#clip0_4824_30258)">
<g id="Group">
<path id="Vector" d="M20.9525 135.993C32.5242 135.993 41.905 126.674 41.905 115.178C41.905 103.682 32.5242 94.3627 20.9525 94.3627C9.38074 94.3627 0 103.682 0 115.178C0 126.674 9.38074 135.993 20.9525 135.993Z" fill="#271B3D"/>
<path id="Vector_2" d="M20.9525 135.993C26.7245 135.993 31.9462 133.676 35.7359 129.926C33.8337 122.573 27.9379 117.212 20.9525 117.212C13.9672 117.212 8.07131 122.573 6.16919 129.926C9.95885 133.676 15.1842 135.993 20.9525 135.993Z" fill="#BEA0F2"/>
<path id="Vector_3" d="M20.9524 114.816C25.1122 114.816 28.4843 111.466 28.4843 107.333C28.4843 103.201 25.1122 99.8506 20.9524 99.8506C16.7926 99.8506 13.4204 103.201 13.4204 107.333C13.4204 111.466 16.7926 114.816 20.9524 114.816Z" fill="#BEA0F2"/>
</g>
<g id="Group_2">
<path id="Vector_4" d="M193.437 136C191.52 136 189.961 134.472 189.931 132.572V60.7075C189.931 58.7853 191.502 57.225 193.437 57.225C195.372 57.225 196.942 58.7853 196.942 60.7075V132.579C196.942 134.472 195.354 136 193.437 136Z" fill="#BEA0F2"/>
<path id="Vector_5" d="M196.942 132.579V79.2817H189.931V132.572C189.961 134.469 191.52 136 193.437 136C195.354 136 196.942 134.476 196.942 132.579Z" fill="#271B3D"/>
</g>
<g id="Group_3">
<path id="Vector_6" d="M174.376 136C172.459 136 170.899 134.472 170.87 132.572V60.7075C170.87 58.7853 172.441 57.225 174.376 57.225C176.31 57.225 177.881 58.7853 177.881 60.7075V132.579C177.881 134.472 176.292 136 174.376 136Z" fill="#BEA0F2"/>
<path id="Vector_7" d="M177.881 132.579V94.1274H170.87V132.572C170.899 134.469 172.459 136 174.376 136C176.292 136 177.881 134.476 177.881 132.579Z" fill="#271B3D"/>
</g>
<g id="Group_4">
<path id="Vector_8" d="M212.494 136C210.578 136 209.018 134.472 208.989 132.572V60.7075C208.989 58.7853 210.56 57.225 212.494 57.225C214.429 57.225 216 58.7853 216 60.7075V132.579C216 134.472 214.411 136 212.494 136Z" fill="#BEA0F2"/>
<path id="Vector_9" d="M216 132.579V98.7791H208.989V132.572C209.018 134.469 210.578 136 212.494 136C214.411 136 216 134.476 216 132.579Z" fill="#271B3D"/>
</g>
<path id="Vector_10" d="M167.798 32.4354V24.6307L161.301 23.7836C160.711 21.0759 159.64 18.5455 158.175 16.2938L162.161 11.1317L156.612 5.61844L151.415 9.57873C149.149 8.11625 146.606 7.04834 143.869 6.47276L143.024 0.0219116H135.167L134.315 6.47638C131.589 7.05558 129.042 8.11987 126.775 9.58235L121.579 5.62206L116.029 11.1353L120.016 16.2975C118.544 18.5491 117.469 21.0759 116.889 23.7873L110.392 24.6343V32.4391L116.889 33.2861C117.472 35.9939 118.544 38.5243 120.016 40.7759L116.029 45.9381L121.579 51.4513L126.775 47.4911C129.042 48.9535 131.585 50.0214 134.315 50.597L135.167 57.0515H143.024L143.876 50.597C146.602 50.0178 149.149 48.9535 151.415 47.4911L156.612 51.4513L162.161 45.9381L158.175 40.7759C159.647 38.5243 160.722 35.9975 161.301 33.2861L167.798 32.4391V32.4354ZM139.099 36.6455C134.253 36.6455 130.321 32.7395 130.321 27.9249C130.321 23.1103 134.253 19.2043 139.099 19.2043C143.945 19.2043 147.877 23.1103 147.877 27.9249C147.877 32.7395 143.945 36.6455 139.099 36.6455Z" fill="#BEA0F2"/>
<g id="Group_5">
<path id="Vector_11" d="M97.613 0H9.37571L0.182129 57.1201H22.8363L22.7489 84.7914L40.6295 57.1201H88.4194L97.613 0Z" fill="#271B3D"/>
<g id="Group_6">
<path id="Vector_12" d="M34.7344 31.552C36.5094 31.552 37.9484 30.1225 37.9484 28.3591C37.9484 26.5958 36.5094 25.1663 34.7344 25.1663C32.9594 25.1663 31.5205 26.5958 31.5205 28.3591C31.5205 30.1225 32.9594 31.552 34.7344 31.552Z" fill="white"/>
<path id="Vector_13" d="M48.8975 31.552C50.6725 31.552 52.1114 30.1225 52.1114 28.3591C52.1114 26.5958 50.6725 25.1663 48.8975 25.1663C47.1225 25.1663 45.6836 26.5958 45.6836 28.3591C45.6836 30.1225 47.1225 31.552 48.8975 31.552Z" fill="white"/>
<path id="Vector_14" d="M63.0606 31.552C64.8356 31.552 66.2745 30.1225 66.2745 28.3591C66.2745 26.5958 64.8356 25.1663 63.0606 25.1663C61.2856 25.1663 59.8467 26.5958 59.8467 28.3591C59.8467 30.1225 61.2856 31.552 63.0606 31.552Z" fill="white"/>
</g>
</g>
<g id="Group_7">
<path id="Vector_15" d="M71.1472 104.017C77.2763 104.017 82.2648 108.969 82.2648 115.062C82.2648 121.154 77.2799 126.107 71.1472 126.107C65.0145 126.107 60.0296 121.154 60.0296 115.062C60.0296 108.969 65.0145 104.017 71.1472 104.017ZM71.1472 94.1274C59.5086 94.1274 50.0781 103.5 50.0781 115.058C50.0781 126.617 59.5122 135.989 71.1472 135.989C82.7822 135.989 92.2163 126.617 92.2163 115.058C92.2163 103.5 82.7822 94.1274 71.1472 94.1274Z" fill="#271B3D"/>
<path id="Vector_16" d="M60.7219 96.875L65.7359 105.422C67.3393 104.531 69.1831 104.017 71.1471 104.017V94.1274C67.3538 94.1274 63.7974 95.1301 60.7219 96.8714V96.875Z" fill="#BEA0F2"/>
</g>
<g id="Group_8">
<path id="Vector_17" d="M121.346 94.1274C109.707 94.1274 100.277 103.5 100.277 115.058C100.277 126.617 109.711 135.989 121.346 135.989C132.981 135.989 142.415 126.617 142.415 115.058C142.415 103.5 132.981 94.1274 121.346 94.1274Z" fill="#BEA0F2"/>
<path id="Vector (Stroke)" fill-rule="evenodd" clip-rule="evenodd" d="M130.941 103.841L118.228 126.872L111.452 115.078L112.357 114.568L118.21 124.755L130.028 103.346L130.941 103.841Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_4824_30258">
<rect width="216" height="136" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,47 @@
<svg width="216" height="136" viewBox="0 0 216 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame" clip-path="url(#clip0_4824_14060)">
<g id="Layer 1">
<path id="Vector" d="M173.664 111.245V103.441L167.168 102.594C166.577 99.886 165.506 97.3558 164.041 95.1043L168.027 89.9426L162.478 84.4298L157.282 88.3898C155.016 86.9274 152.473 85.8596 149.736 85.284L148.891 78.8301H141.035L140.183 85.284C137.457 85.8632 134.91 86.9274 132.644 88.3898L127.448 84.4298L121.899 89.9426L125.885 95.1043C124.413 97.3558 123.338 99.8823 122.759 102.594L116.262 103.441V111.245L122.759 112.092C123.342 114.799 124.413 117.329 125.885 119.581L121.899 124.743L127.448 130.255L132.644 126.295C134.91 127.758 137.454 128.826 140.183 129.401L141.035 135.855H148.891L149.744 129.401C152.469 128.822 155.016 127.758 157.282 126.295L162.478 130.255L168.027 124.743L164.041 119.581C165.513 117.329 166.588 114.803 167.168 112.092L173.664 111.245ZM144.967 115.454C140.121 115.454 136.189 111.549 136.189 106.734C136.189 101.92 140.121 98.0146 144.967 98.0146C149.813 98.0146 153.744 101.92 153.744 106.734C153.744 111.549 149.813 115.454 144.967 115.454Z" fill="#BEA0F2"/>
<path id="Vector_2" d="M0.149414 18.3374H109.004V62.7477C109.004 64.3476 107.699 65.6435 106.089 65.6435H3.06434C1.45385 65.6435 0.149414 64.3476 0.149414 62.7477V18.3374Z" fill="#BEA0F2"/>
<g id="Group">
<path id="Vector_3" d="M3.06434 0H106.089C107.699 0 109.004 1.29586 109.004 2.89577V18.3918H0.149414V2.89577C0.149414 1.29586 1.45385 0 3.06434 0Z" fill="#271B3D"/>
<path id="Vector_4" d="M7.4477 11.0327C8.57259 11.0327 9.4845 10.1268 9.4845 9.00926C9.4845 7.89176 8.57259 6.98584 7.4477 6.98584C6.3228 6.98584 5.41089 7.89176 5.41089 9.00926C5.41089 10.1268 6.3228 11.0327 7.4477 11.0327Z" fill="white"/>
<path id="Vector_5" d="M14.622 11.0327C15.7469 11.0327 16.6588 10.1268 16.6588 9.00926C16.6588 7.89176 15.7469 6.98584 14.622 6.98584C13.4971 6.98584 12.5852 7.89176 12.5852 9.00926C12.5852 10.1268 13.4971 11.0327 14.622 11.0327Z" fill="white"/>
<path id="Vector_6" d="M21.7963 11.0327C22.9212 11.0327 23.8331 10.1268 23.8331 9.00926C23.8331 7.89176 22.9212 6.98584 21.7963 6.98584C20.6714 6.98584 19.7595 7.89176 19.7595 9.00926C19.7595 10.1268 20.6714 11.0327 21.7963 11.0327Z" fill="white"/>
</g>
<path id="Vector_7" d="M97.1911 118.173L106.876 113.54L109.266 103.129L102.565 94.7822H91.8167L85.116 103.129L87.5062 113.54L97.1911 118.173Z" fill="#BEA0F2"/>
<g id="Group_2">
<g id="Group_3">
<path id="Vector_8" d="M191.351 0C176.259 0 162.194 30.9522 168.082 58.0277C172.065 76.3326 181.538 74.2512 181.538 88.9147H201.178C201.178 74.2512 210.651 76.3326 214.634 58.0277C220.518 30.9522 206.454 0 191.351 0Z" fill="#BEA0F2"/>
<path id="Vector_9" d="M179.855 88.3501V94.9706H182.73C182.73 99.5242 186.071 103.64 191.354 103.64C196.637 103.64 199.979 99.5278 199.979 94.9706H202.854V88.3501H179.855Z" fill="#271B3D"/>
</g>
<path id="Vector_10" d="M194.003 88.8639C194.003 80.5639 192.976 72.0467 191.354 64.2824C189.736 72.043 188.709 80.5639 188.709 88.8639H187.317C187.317 79.6517 189.008 70.2042 190.83 61.8065C190.298 59.3668 189.755 57.0104 189.241 54.7806C186.774 44.0988 184.825 35.6613 187.35 32.0886C188.21 30.8688 189.558 30.2534 191.358 30.2534C193.158 30.2534 194.506 30.8688 195.366 32.0886C197.891 35.6613 195.942 44.0952 193.479 54.7806C192.961 57.0104 192.422 59.3632 191.89 61.8065C193.712 70.2042 195.402 79.6517 195.402 88.8639H194.007H194.003ZM191.354 32.6135C189.868 32.6135 188.822 33.0696 188.159 34.0107C185.943 37.1417 187.835 44.1206 190.232 54.4983C190.596 56.0765 190.975 57.7162 191.354 59.403C191.733 57.7126 192.112 56.0765 192.477 54.4983C194.874 44.1169 196.765 37.1454 194.55 34.0107C193.887 33.0732 192.841 32.6135 191.354 32.6135Z" fill="#271B3D"/>
</g>
<g id="Group_4">
<path id="Vector_11" d="M4.43069 136L0 131.599L56.2436 77.7734L58.612 80.1263L4.43069 136Z" fill="#BEA0F2"/>
<path id="Vector_12" d="M45.8408 93.3817L58.6119 80.1263L56.2435 77.7734L42.9004 90.4605L45.8408 93.3817Z" fill="#271B3D"/>
</g>
<g id="Group_5">
<g id="Group_6">
<path id="Vector_13" d="M54.6658 40.2295H5.29785V43.8492H54.6658V40.2295Z" fill="#271B3D"/>
<path id="Vector_14" d="M36.269 50.0967H5.29785V53.7164H36.269V50.0967Z" fill="#271B3D"/>
</g>
<path id="Vector_15" d="M54.6658 30.3623H5.29785V33.982H54.6658V30.3623Z" fill="#271B3D"/>
</g>
</g>
<g id="Layer 4">
<g id="Group_7">
<path id="Vector_16" d="M61.986 65.5241C58.9108 68.5792 53.6311 68.9013 53.6311 70.1248C53.6311 71.3483 58.9108 71.6704 61.986 74.7255C65.0613 77.7805 65.3856 83.0255 66.6171 83.0255C67.8487 83.0255 68.173 77.7805 71.2482 74.7255C74.3235 71.6704 79.6031 71.3483 79.6031 70.1248C79.6031 68.9013 74.3235 68.5792 71.2482 65.5241C68.173 62.4691 67.8487 57.2241 66.6171 57.2241C65.3856 57.2241 65.0613 62.4691 61.986 65.5241Z" fill="#271B3D"/>
<g id="Group_8">
<path id="Vector_17" d="M81.2123 84.6206C82.8316 83.0119 83.3091 80.8782 82.2789 79.8548C81.2487 78.8313 79.1009 79.3057 77.4816 80.9144C75.8623 82.5231 75.3847 84.6568 76.4149 85.6802C77.4451 86.7037 79.593 86.2293 81.2123 84.6206Z" fill="#271B3D"/>
<path id="Vector_18" d="M86.4712 89.8473C87.5782 88.7475 87.9047 87.2888 87.2003 86.589C86.496 85.8893 85.0276 86.2136 83.9205 87.3134C82.8135 88.4131 82.487 89.8719 83.1914 90.5717C83.8957 91.2714 85.3642 90.9471 86.4712 89.8473Z" fill="#271B3D"/>
</g>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_4824_14060">
<rect width="216" height="136" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,83 @@
<svg width="210" height="136" viewBox="0 0 210 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Layout" clip-path="url(#clip0_4824_27305)">
<rect width="209" height="136" transform="translate(0.5)" fill="white"/>
<path id="Vector" d="M209.47 131.58H205.405V135.661H209.47V131.58Z" fill="white"/>
<path id="Vector_2" d="M195.273 14.28V48.28H198.661V14.28H195.273Z" fill="#BEA0F2"/>
<path id="Vector_3" d="M202.048 4.08003V48.28H205.435V4.08003H202.048Z" fill="#BEA0F2"/>
<path id="Vector_4" d="M188.498 24.14V48.28H191.886V24.14H188.498Z" fill="#BEA0F2"/>
<path id="Vector_5" d="M86.0894 128.68L94.2783 106.08H115.383L123.572 128.68H139.382V131.92H104.831H70.2795V128.68H86.0894Z" fill="#BEA0F2"/>
<g id="Group">
<path id="Vector_6" d="M89.828 38.1243H32.2429V95.9243H89.828V38.1243Z" fill="#BEA0F2"/>
<g id="Group_2">
<g id="Group_3">
<path id="Vector_7" d="M46.6493 67.8674L39.8238 70.3426C37.3137 71.2538 37.0732 72.8654 37.7202 73.9636C38.5027 75.2862 40.3691 75.1604 42.4083 73.7936C44.3086 72.5186 47.2217 70.4786 48.1668 69.7952L46.6493 67.8674Z" fill="#271B3D"/>
<path id="Vector_8" d="M75.4182 67.8674C75.4182 67.8674 81.1089 69.9312 82.2437 70.3426C84.7537 71.2538 84.9942 72.8654 84.3472 73.9636C83.5648 75.2862 81.6983 75.1604 79.6591 73.7936C77.7588 72.5186 74.8457 70.4786 73.9006 69.7952L75.4182 67.8674Z" fill="#271B3D"/>
</g>
<path id="Vector_9" d="M45.1353 61.8055L53.5122 73.0221L53.8611 72.8249L51.6187 60.0681L45.1353 61.8055Z" fill="white"/>
<g id="Group_4">
<g id="Group_5">
<path id="Vector_10" d="M50.6363 72.76C46.9203 72.76 44.4272 69.462 44.4272 66.1538C44.4272 62.5396 47.6723 61.1218 50.7006 61.1218C53.7289 61.1218 57.1603 62.7096 57.1603 65.9226C57.1603 69.6082 54.464 72.76 50.6363 72.76Z" fill="#BEA0F2"/>
<path id="Vector_11" d="M52.7738 72.4064C53.0109 72.3248 53.2379 72.2296 53.458 72.1208L51.5306 61.1626C51.2529 61.1354 50.9751 61.1218 50.7007 61.1218C48.8411 61.1218 46.9001 61.659 45.6807 62.9068L52.7738 72.4064Z" fill="white"/>
</g>
<g id="Group_6">
<g id="Group_7">
<path id="Vector_12" d="M64.9072 65.9226C64.9072 62.7096 68.3386 61.1218 71.3669 61.1218C74.3952 61.1218 77.6403 62.5396 77.6403 66.1538C77.6403 69.4654 75.1438 72.76 71.4313 72.76C67.6036 72.76 64.9072 69.6082 64.9072 65.9226Z" fill="#BEA0F2"/>
<path id="Vector_13" d="M69.2939 72.4064C69.0567 72.3248 68.8298 72.2296 68.6096 72.1208L70.537 61.1626C70.8148 61.1354 71.0926 61.1218 71.3669 61.1218C73.2266 61.1218 75.1675 61.659 76.387 62.9068L69.2939 72.4064Z" fill="white"/>
</g>
<path id="Vector_14" d="M71.4957 59.1567C64.7345 59.1567 62.8681 64.0799 61.0321 64.0799C59.1962 64.0799 57.3331 59.1567 50.5686 59.1567C46.287 59.1567 41.8123 61.9209 39.4885 63.0429V66.4123H41.985C42.3305 68.5713 44.0682 74.8035 50.6329 74.8035C57.97 74.8035 58.1325 67.3031 61.0287 67.3031C63.9249 67.3031 64.0909 74.8035 71.4245 74.8035C77.9892 74.8035 79.727 68.5713 80.0725 66.4123H82.5689V63.0429C80.2452 61.9209 75.7705 59.1567 71.4889 59.1567H71.4957ZM50.6363 72.7601C46.9204 72.7601 44.4273 69.4621 44.4273 66.1539C44.4273 62.5397 47.6724 61.1219 50.7007 61.1219C53.729 61.1219 57.1604 62.7097 57.1604 65.9227C57.1604 69.6083 54.464 72.7601 50.6363 72.7601ZM71.4313 72.7601C67.6036 72.7601 64.9073 69.6083 64.9073 65.9227C64.9073 62.7097 68.3387 61.1219 71.367 61.1219C74.3953 61.1219 77.6403 62.5397 77.6403 66.1539C77.6403 69.4655 75.1439 72.7601 71.4313 72.7601Z" fill="#271B3D"/>
</g>
</g>
</g>
</g>
<g id="Group_8">
<path id="Vector_15" d="M151.539 44.9617H93.9539V48.3625H151.539V44.9617Z" fill="#271B3D"/>
<path id="Vector_16" d="M122.746 51.6121H93.9539V55.0128H122.746V51.6121Z" fill="#271B3D"/>
<path id="Vector_17" d="M151.539 38.1616H93.9539V41.5624H151.539V38.1616Z" fill="#271B3D"/>
</g>
<path id="Vector_18" d="M154.879 71.5225V80.8623H170.484C170.484 80.8623 168.808 84.2929 168.808 88.1315C168.808 91.9701 171.277 95.8257 176.036 95.8257C180.796 95.8257 183.265 91.8409 183.265 88.1315C183.265 84.4221 181.588 80.8623 181.588 80.8623H192.997V74.9497V71.5225C198.684 74.7525 205.34 73.6577 205.34 66.9529C205.34 60.2481 198.684 59.1533 192.997 62.3833V53.0435H181.588C181.588 53.0435 183.265 49.6129 183.265 45.7743C183.265 41.9357 180.796 38.0801 176.036 38.0801C171.277 38.0801 168.808 42.0649 168.808 45.7743C168.808 49.4837 170.484 53.0435 170.484 53.0435H154.879V62.3833C160.566 59.1533 167.222 60.2481 167.222 66.9529C167.222 73.6577 160.566 74.7525 154.879 71.5225Z" fill="#271B3D"/>
<path id="Vector_19" d="M151.539 87.3799H93.9539V95.8799H151.539V87.3799Z" fill="#BEA0F2"/>
<path id="Vector_20" d="M33.3572 119.107V115.153L30.0974 114.725C29.8049 113.352 29.2692 112.072 28.5291 110.929L30.5308 108.311L27.7467 105.517L25.1389 107.526C24.0005 106.783 22.7248 106.246 21.3574 105.952L20.931 102.68H16.991L16.5645 105.952C15.1971 106.246 13.9214 106.783 12.7831 107.526L10.1752 105.517L7.39109 108.311L9.39282 110.929C8.65275 112.072 8.11707 113.352 7.82456 114.725L4.5647 115.153V119.107L7.82456 119.535C8.12059 120.908 8.65979 122.188 9.39282 123.331L7.39109 125.948L10.1752 128.743L12.7831 126.734C13.9214 127.477 15.1971 128.014 16.5681 128.308L16.991 131.58H20.931L21.3574 128.308C22.7248 128.014 24.0005 127.477 25.1389 126.734L27.7467 128.743L30.5308 125.948L28.5291 123.331C29.2692 122.188 29.8049 120.908 30.0974 119.535L33.3572 119.107ZM19.0209 123.419C15.391 123.419 12.4483 120.466 12.4483 116.822C12.4483 113.179 15.391 110.225 19.0209 110.225C22.6508 110.225 25.5935 113.179 25.5935 116.822C25.5935 120.466 22.6508 123.419 19.0209 123.419Z" fill="#271B3D"/>
<g id="Group_9">
<path id="Vector_21" d="M177.699 24.1638H32.2429V34.3638H177.699V24.1638Z" fill="#271B3D"/>
<g id="Group_10">
<path id="Vector_22" d="M36.2705 30.7504C37.1535 30.7504 37.8693 30.0317 37.8693 29.1452C37.8693 28.2587 37.1535 27.54 36.2705 27.54C35.3875 27.54 34.6716 28.2587 34.6716 29.1452C34.6716 30.0317 35.3875 30.7504 36.2705 30.7504Z" fill="white"/>
<path id="Vector_23" d="M42.0661 30.7504C42.9491 30.7504 43.665 30.0317 43.665 29.1452C43.665 28.2587 42.9491 27.54 42.0661 27.54C41.1831 27.54 40.4673 28.2587 40.4673 29.1452C40.4673 30.0317 41.1831 30.7504 42.0661 30.7504Z" fill="white"/>
<path id="Vector_24" d="M47.8654 30.7504C48.7484 30.7504 49.4643 30.0317 49.4643 29.1452C49.4643 28.2587 48.7484 27.54 47.8654 27.54C46.9824 27.54 46.2666 28.2587 46.2666 29.1452C46.2666 30.0317 46.9824 30.7504 47.8654 30.7504Z" fill="white"/>
</g>
</g>
<path id="Vector_25" d="M32.3411 100.395V101.213C32.3411 104.14 34.7766 106.515 37.7845 106.515H172.354C175.359 106.515 177.798 104.143 177.798 101.213V100.395H32.3411Z" fill="#BEA0F2"/>
<path id="Vector_26" d="M177.798 20.4V19.5818C177.798 16.6554 175.362 14.28 172.354 14.28H37.7845C34.78 14.28 32.3411 16.6521 32.3411 19.5818V20.4H177.798Z" fill="#BEA0F2"/>
<g id="Group_11">
<path id="Vector_27" d="M205.344 123.407C205.344 127.878 201.767 131.512 197.329 131.58H163.022C158.524 131.58 154.879 127.922 154.879 123.407C154.879 118.891 158.524 115.233 163.022 115.233H197.346C201.777 115.233 205.347 118.942 205.347 123.407H205.344Z" fill="#BEA0F2"/>
<path id="Vector_28" d="M197.16 129.217C200.357 129.217 202.949 126.616 202.949 123.407C202.949 120.197 200.357 117.596 197.16 117.596C193.963 117.596 191.371 120.197 191.371 123.407C191.371 126.616 193.963 129.217 197.16 129.217Z" fill="#271B3D"/>
</g>
<g id="Group_12">
<path id="Vector_29" d="M113.15 63.886H93.9539V83.1538H113.15V63.886Z" fill="#BEA0F2"/>
<g id="Group_13">
<path id="Vector_30" d="M98.3474 73.5183H108.757" stroke="#271B3D" stroke-width="0.908286" stroke-miterlimit="10"/>
<path id="Vector_31" d="M103.55 68.2959V78.7441" stroke="#271B3D" stroke-width="0.908286" stroke-miterlimit="10"/>
</g>
</g>
<g id="Group 1000005542">
<g id="Group_14">
<path id="Vector_32" d="M7.82334 98.0595C6.04159 98.0595 4.5918 96.6247 4.5647 94.8397V7.35088C4.5647 5.54548 6.02465 4.08008 7.82334 4.08008C9.62202 4.08008 11.082 5.54548 11.082 7.35088V94.8465C11.082 96.6247 9.60509 98.0595 7.82334 98.0595Z" fill="#BEA0F2"/>
<path id="Vector_33" d="M11.082 33.1501V29.8385L4.5647 25.3301V28.6417L11.082 33.1501Z" fill="#271B3D"/>
<path id="Vector_34" d="M11.082 39.5795V36.2679L4.5647 31.7595V35.0711L11.082 39.5795Z" fill="#271B3D"/>
<path id="Vector_35" d="M11.082 46.0056V42.6974L4.5647 38.189V41.5006L11.082 46.0056Z" fill="#271B3D"/>
<path id="Vector_36" d="M11.082 52.4348V49.1266L4.5647 44.6182V47.9298L11.082 52.4348Z" fill="#271B3D"/>
<path id="Vector_37" d="M11.082 58.8642V55.556L4.5647 51.0476V54.3592L11.082 58.8642Z" fill="#271B3D"/>
<path id="Vector_38" d="M11.082 65.2936V61.9854L4.5647 57.4771V60.7852L11.082 65.2936Z" fill="#271B3D"/>
<path id="Vector_39" d="M11.082 71.7231V68.4115L4.5647 63.9065V67.2147L11.082 71.7231Z" fill="#271B3D"/>
<path id="Vector_40" d="M11.082 78.1525V74.8409L4.5647 70.3359V73.6441L11.082 78.1525Z" fill="#271B3D"/>
<path id="Vector_41" d="M11.082 84.582V81.2704L4.5647 76.7654V80.0736L11.082 84.582Z" fill="#271B3D"/>
<path id="Vector_42" d="M11.082 91.0112V87.6996L4.5647 83.1912V86.5028L11.082 91.0112Z" fill="#271B3D"/>
<path id="Vector_43" d="M4.5647 89.6206V92.9322L10.3164 96.9102C10.794 96.3526 11.082 95.6352 11.082 94.8464V94.129L4.5647 89.6206Z" fill="#271B3D"/>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_4824_27305">
<rect width="209" height="136" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,61 @@
<svg width="216" height="136" viewBox="0 0 216 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Comparing" clip-path="url(#clip0_4824_20218)">
<g id="Layer 1">
<g id="Group">
<path id="Vector" d="M0 131.57C0 129.147 1.95966 127.18 4.39285 127.14H118.702C121.168 127.14 123.164 129.122 123.164 131.57C123.164 134.018 121.168 136 118.702 136H4.38192C1.95602 135.996 0 133.989 0 131.57Z" fill="#BEA0F2"/>
<g id="Group_2">
<path id="Vector_2" d="M9.9585 135.996H13L16.668 127.14H13.6228L9.9585 135.996Z" fill="#271B3D"/>
<path id="Vector_3" d="M22.0845 135.996H25.126L28.794 127.14H25.7488L22.0845 135.996Z" fill="#271B3D"/>
<path id="Vector_4" d="M19.6878 127.14L16.0198 135.996H19.0649L22.7329 127.14H19.6878Z" fill="#271B3D"/>
<path id="Vector_5" d="M31.8682 127.14L28.2002 135.996H31.2453L34.9133 127.14H31.8682Z" fill="#271B3D"/>
<path id="Vector_6" d="M37.9585 127.14L34.2905 135.996H37.3357L41.0036 127.14H37.9585Z" fill="#271B3D"/>
<path id="Vector_7" d="M44.0486 127.14L40.3806 135.996H43.4257L47.0937 127.14H44.0486Z" fill="#271B3D"/>
<path id="Vector_8" d="M50.139 127.14L46.4746 135.996H49.5161L53.1841 127.14H50.139Z" fill="#271B3D"/>
<path id="Vector_9" d="M56.2293 127.14L52.5649 135.996H55.6064L59.2744 127.14H56.2293Z" fill="#271B3D"/>
<path id="Vector_10" d="M62.3013 127.14L58.6333 135.996H61.6784L65.3464 127.14H62.3013Z" fill="#271B3D"/>
<path id="Vector_11" d="M68.3733 127.14L64.7053 135.996H67.7468L71.4148 127.14H68.3733Z" fill="#271B3D"/>
<path id="Vector_12" d="M74.4417 127.14L70.7737 135.996H73.8188L77.4868 127.14H74.4417Z" fill="#271B3D"/>
<path id="Vector_13" d="M80.5137 127.14L76.8457 135.996H79.8908L83.5552 127.14H80.5137Z" fill="#271B3D"/>
<path id="Vector_14" d="M86.5821 127.14L82.9177 135.996H85.9592L89.6272 127.14H86.5821Z" fill="#271B3D"/>
<path id="Vector_15" d="M10.6033 127.14H7.55819L3.90112 135.971C4.05775 135.989 4.21802 135.996 4.37829 135.996H6.93532L10.6033 127.137V127.14Z" fill="#271B3D"/>
<path id="Vector_16" d="M1.37686 134.785L4.54219 127.14H4.39285C2.72823 127.166 1.2858 128.099 0.539089 129.458L0.178482 130.326C0.0619224 130.72 0 131.136 0 131.567C0 132.825 0.528162 133.971 1.37686 134.781V134.785Z" fill="#271B3D"/>
</g>
</g>
<g id="Group_3">
<path id="Vector_17" d="M123.131 18.1391V61.5016H126.773V18.1391H123.131Z" fill="#BEA0F2"/>
<path id="Vector_18" d="M131.075 -1.90735e-05V61.5016H134.718V-1.90735e-05H131.075Z" fill="#BEA0F2"/>
<path id="Vector_19" d="M139.023 33.8951V61.5016H142.666V33.8951H139.023Z" fill="#BEA0F2"/>
</g>
<g id="Group_4">
<path id="Vector_20" d="M216 72.7012H154.078V76.2668H216V72.7012Z" fill="#271B3D"/>
<path id="Vector_21" d="M201.175 80.5304H154.078V84.096H201.175V80.5304Z" fill="#271B3D"/>
</g>
<path id="Vector_22" d="M180.941 80.5303H154.078V89.8891H180.941V80.5303Z" fill="#271B3D"/>
<path id="Vector_23" d="M75.8039 95.2954H130.715C132.649 95.2954 134.219 96.854 134.219 98.7743C134.219 100.694 132.649 102.253 130.715 102.253H75.7966L75.8039 95.2954Z" fill="#BEA0F2"/>
<path id="Vector_24" d="M159.498 76.3137C171.973 76.3137 182.121 86.3886 182.121 98.7742C182.121 111.16 171.973 121.235 159.498 121.235C147.022 121.235 136.874 111.16 136.874 98.7742C136.874 86.3886 147.022 76.3137 159.498 76.3137ZM159.498 69.8044C143.383 69.8044 130.318 82.7759 130.318 98.7742C130.318 114.773 143.383 127.744 159.498 127.744C175.612 127.744 188.678 114.773 188.678 98.7742C188.678 82.7759 175.612 69.8044 159.498 69.8044Z" fill="#BEA0F2"/>
</g>
<g id="Layer 4">
<g id="Group_5">
<path id="Vector_25" d="M216 0H154.078V61.5016H216V0Z" fill="#271B3D"/>
<g id="Group_6">
<path id="Vector_26" d="M193.082 19.3649H177.029V10.5521C184.533 14.049 193.082 10.5521 193.082 10.5521V19.3649Z" fill="#BEA0F2"/>
<path id="Vector_27" d="M192.331 10.422C192.331 13.8574 190.976 17.3218 185.039 17.3218C179.102 17.3218 177.747 13.8574 177.747 10.422C169.653 10.422 160.03 16.2188 157.451 18.7792L164.889 30.3114L169.194 29.2084L169.26 52.5333H200.811L200.877 29.2084L205.182 30.3114L212.62 18.7792C210.041 16.2188 200.418 10.422 192.324 10.422H192.331Z" fill="white"/>
</g>
</g>
<g id="Group_7">
<g id="Group_8">
<path id="Vector_28" d="M61.9224 90.7607H0V94.377H61.9224V90.7607Z" fill="#271B3D"/>
<path id="Vector_29" d="M45.1451 98.6478H0V102.264H45.1451V98.6478Z" fill="#271B3D"/>
</g>
<path id="Vector_30" d="M61.9224 18.1392H0V79.6154H61.9224V18.1392Z" fill="#BEA0F2"/>
</g>
<path id="Vector_31" d="M8.29044 44.2556C9.77658 46.3892 18.2454 47.7019 25.1443 46.0204C23.221 42.7585 24.0297 41.1384 24.8565 41.0589C26.2042 40.9251 30.8739 45.1742 34.7167 46.9968C39.299 49.1701 43.5316 50.2875 46.9155 50.9421C56.9032 52.8804 56.3422 58.0806 56.0436 61.2629H6.47284C5.48208 56.1495 5.31452 48.6855 8.29044 44.2556Z" fill="white"/>
<path id="Vector_32" d="M56.0361 57.6431H5.96997C6.08289 58.9232 6.25773 60.1491 6.47264 61.2629H56.0434C56.1417 60.2106 56.2655 58.9413 56.0324 57.6431H56.0361Z" fill="#271B3D"/>
</g>
</g>
<defs>
<clipPath id="clip0_4824_20218">
<rect width="216" height="136" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,31 @@
<svg width="216" height="136" viewBox="0 0 216 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M209.249 86.6924H173.087C169.359 86.6924 166.336 89.6934 166.336 93.3953V129.297C166.336 132.999 169.359 136 173.087 136H209.249C212.978 136 216 132.999 216 129.297V93.3953C216 89.6934 212.978 86.6924 209.249 86.6924Z" fill="#BEA0F2"/>
<path d="M190.12 120.091L187.869 114.687H176.556L174.304 120.091H173.178L181.631 99.9536H182.786L191.239 120.091H190.113H190.12ZM182.214 101.071L176.924 113.783H187.504L182.214 101.071Z" fill="#271B3D"/>
<path d="M203.988 120.091V118.312C202.618 119.791 200.978 120.457 198.909 120.457C196.325 120.457 193.768 118.706 193.768 115.595C193.768 112.484 196.293 110.733 198.909 110.733C200.978 110.733 202.618 111.399 203.988 112.878V109.587C203.988 107.232 202.104 105.962 199.852 105.962C197.936 105.962 196.507 106.628 195.138 108.255L194.438 107.651C195.866 105.991 197.389 105.145 199.852 105.145C202.741 105.145 204.898 106.595 204.898 109.554V120.091H203.988ZM203.988 117.404V113.779C202.862 112.3 201.069 111.544 199.153 111.544C196.478 111.544 194.773 113.323 194.773 115.588C194.773 117.852 196.475 119.632 199.153 119.632C201.069 119.632 202.862 118.876 203.988 117.396V117.404Z" fill="#271B3D"/>
<path d="M129.784 120.091L127.992 115.591H117.896L116.103 120.091H113.214L121.393 99.9536H124.527L132.677 120.091H129.788H129.784ZM122.942 102.522L118.654 113.392H127.259L122.942 102.522Z" fill="#271B3D"/>
<path d="M144.413 120.091V118.431C143.229 119.758 141.524 120.453 139.549 120.453C137.086 120.453 134.441 118.793 134.441 115.624C134.441 112.455 137.057 110.824 139.549 110.824C141.557 110.824 143.229 111.457 144.413 112.784V110.158C144.413 108.226 142.832 107.109 140.704 107.109C138.941 107.109 137.512 107.713 136.204 109.131L135.14 107.561C136.722 105.929 138.605 105.145 141.01 105.145C144.144 105.145 146.698 106.534 146.698 110.064V120.088H144.417L144.413 120.091ZM144.413 117.013V114.235C143.531 113.056 141.979 112.455 140.398 112.455C138.237 112.455 136.747 113.783 136.747 115.624C136.747 117.465 138.237 118.825 140.398 118.825C141.979 118.825 143.531 118.221 144.413 117.013Z" fill="#271B3D"/>
<path d="M215.92 0H112.507V10.8519H215.92V0Z" fill="#271B3D"/>
<path d="M215.92 20.6909H112.507V31.5428H215.92V20.6909Z" fill="#271B3D"/>
<path d="M180.655 43.3752H112.507V54.2272H180.655V43.3752Z" fill="#271B3D"/>
<path d="M90.3437 124.11H0V129.536H90.3437V124.11Z" fill="#271B3D"/>
<path d="M21.9448 121.024V132.625C21.9448 135.107 24.1091 135.92 26.7396 135.92C29.3702 135.92 31.5345 135.107 31.5345 132.625V121.024C31.5345 118.543 29.3702 117.729 26.7396 117.729C24.1091 117.729 21.9448 118.543 21.9448 121.024Z" fill="#BEA0F2"/>
<path d="M90.3437 102.305H0V107.731H90.3437V102.305Z" fill="#271B3D"/>
<path d="M60.5037 99.2192V110.82C60.5037 113.301 62.6679 114.115 65.2985 114.115C67.9291 114.115 70.0933 113.301 70.0933 110.82V99.2192C70.0933 96.7377 67.9291 95.9238 65.2985 95.9238C62.6679 95.9238 60.5037 96.7377 60.5037 99.2192Z" fill="#BEA0F2"/>
<path d="M112.762 50.4471C108.415 50.4471 104.451 46.9672 103.581 47.8354C102.71 48.6999 106.211 52.6355 106.211 56.951C106.211 61.2664 102.706 65.2021 103.581 66.0666C104.455 66.9311 108.415 63.4549 112.762 63.4549C117.109 63.4549 121.073 66.9348 121.944 66.0666C122.814 65.2021 119.313 61.2664 119.313 56.951C119.313 52.6355 122.818 48.6999 121.944 47.8354C121.069 46.9708 117.109 50.4471 112.762 50.4471Z" fill="#BEA0F2"/>
<path d="M128.603 72.343C130.222 70.7354 130.699 68.6031 129.669 67.5803C128.639 66.5575 126.491 67.0317 124.872 68.6392C123.253 70.2468 122.775 72.3792 123.805 73.4019C124.836 74.4247 126.983 73.9506 128.603 72.343Z" fill="#BEA0F2"/>
<path d="M133.861 77.5667C134.968 76.4676 135.295 75.0098 134.59 74.3106C133.886 73.6113 132.418 73.9354 131.311 75.0344C130.204 76.1335 129.877 77.5913 130.581 78.2905C131.286 78.9898 132.754 78.6657 133.861 77.5667Z" fill="#BEA0F2"/>
<path d="M88.9409 89.9298H2.84547C1.95282 89.9298 1.22412 89.21 1.22412 88.3201V2.8431C1.22412 1.95686 1.94917 1.2334 2.84547 1.2334H88.9409C89.8335 1.2334 90.5622 1.95324 90.5622 2.8431V88.3201C90.5622 89.2064 89.8372 89.9298 88.9409 89.9298ZM2.84547 1.59513C2.15321 1.59513 1.58847 2.15581 1.58847 2.8431V88.3201C1.58847 89.0074 2.15321 89.5681 2.84547 89.5681H88.9409C89.6331 89.5681 90.1979 89.0074 90.1979 88.3201V2.8431C90.1979 2.15581 89.6331 1.59513 88.9409 1.59513H2.84547Z" fill="#271B3D"/>
<path d="M91.7829 1.42703C91.7829 0.638902 91.1394 0 90.3456 0C89.5517 0 88.9082 0.638902 88.9082 1.42703C88.9082 2.21515 89.5517 2.85406 90.3456 2.85406C91.1394 2.85406 91.7829 2.21515 91.7829 1.42703Z" fill="#271B3D"/>
<path d="M2.8747 1.42703C2.8747 0.638902 2.23118 0 1.43735 0C0.643524 0 0 0.638902 0 1.42703C0 2.21515 0.643524 2.85406 1.43735 2.85406C2.23118 2.85406 2.8747 2.21515 2.8747 1.42703Z" fill="#271B3D"/>
<path d="M91.7829 89.7146C91.7829 88.9265 91.1394 88.2876 90.3456 88.2876C89.5517 88.2876 88.9082 88.9265 88.9082 89.7146C88.9082 90.5027 89.5517 91.1417 90.3456 91.1417C91.1394 91.1417 91.7829 90.5027 91.7829 89.7146Z" fill="#271B3D"/>
<path d="M2.8747 89.7146C2.8747 88.9265 2.23118 88.2876 1.43735 88.2876C0.643524 88.2876 0 88.9265 0 89.7146C0 90.5027 0.643524 91.1417 1.43735 91.1417C2.23118 91.1417 2.8747 90.5027 2.8747 89.7146Z" fill="#271B3D"/>
<path d="M150.997 128.59H109.312V129.536H150.997V128.59Z" fill="#271B3D"/>
<g clip-path="url(#clip0_4824_29746)">
<path d="M34.0241 4.98413C28.7538 4.98413 24.1391 13.0249 21.6276 25.012C20.0688 13.0249 17.2109 4.98413 13.9324 4.98413C8.99608 4.98413 5 23.2855 5 45.8539C5 68.4223 8.99608 86.7237 13.9324 86.7237C17.1986 86.7237 20.0564 78.6829 21.6153 66.6958C24.1267 78.6829 28.7414 86.7237 34.0118 86.7237C38.7873 86.7237 43.0184 80.1382 45.6412 70.0009C49.8105 80.1382 56.5284 86.7237 64.0999 86.7237C76.7438 86.7237 86.9876 68.4223 86.9876 45.8539C86.9876 23.2855 76.7562 4.98413 64.1122 4.98413C56.5284 4.98413 49.8105 11.5697 45.6536 21.7069C43.0308 11.5697 38.7996 4.98413 34.0241 4.98413Z" fill="#BEA0F2"/>
</g>
<defs>
<clipPath id="clip0_4824_29746">
<rect width="82" height="81.7396" fill="white" transform="translate(5 4.98413)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { assign } from 'xstate';
import { getQuery, updateQueryString } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -75,10 +76,34 @@ const logAIAPIRequestError = () => {
console.log( 'API Request error' );
};
const updateQueryStep = (
_context: unknown,
_evt: unknown,
{ action }: { action: unknown }
) => {
const { path } = getQuery() as { path: string };
const step = ( action as { step: string } ).step;
const pathFragments = path.split( '/' ); // [0] '', [1] 'customize-store', [2] cys step slug [3] design-with-ai step slug
if (
pathFragments[ 1 ] === 'customize-store' &&
pathFragments[ 2 ] === 'design-with-ai'
) {
if ( pathFragments[ 3 ] !== step ) {
// this state machine is only concerned with [2], so we ignore changes to [3]
// [1] is handled by router at root of wc-admin
updateQueryString(
{},
`/customize-store/design-with-ai/${ step }`
);
}
}
};
export const actions = {
assignBusinessInfoDescription,
assignLookAndFeel,
assignToneOfVoice,
assignLookAndTone,
logAIAPIRequestError,
updateQueryStep,
};

View File

@ -1,17 +1,94 @@
/**
* External dependencies
*/
import { Loader } from '@woocommerce/onboarding';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
import analyzingYourResponses from '../../assets/images/loader-analyzing-your-responses.svg';
import designingTheBestLook from '../../assets/images/loader-designing-the-best-look.svg';
import comparingTheTopPerformingStores from '../../assets/images/loader-comparing-top-performing-stores.svg';
import assemblingAiOptimizedStore from '../../assets/images/loader-assembling-ai-optimized-store.svg';
import applyingFinishingTouches from '../../assets/images/loader-applying-the-finishing-touches.svg';
export const ApiCallLoader = ( {
context,
}: {
context: designWithAiStateMachineContext;
} ) => {
const loaderSteps = [
{
title: __( 'Analyzing your responses', 'woocommerce' ),
image: (
<img
src={ analyzingYourResponses }
alt={ __( 'Analyzing your responses', 'woocommerce' ) }
/>
),
progress: 17,
},
{
title: __( 'Comparing the top performing stores', 'woocommerce' ),
image: (
<img
src={ comparingTheTopPerformingStores }
alt={ __(
'Comparing the top performing stores',
'woocommerce'
) }
/>
),
progress: 33,
},
{
title: __( 'Designing the best look for your business', 'woocommerce' ),
image: (
<img
src={ designingTheBestLook }
alt={ __(
'Designing the best look for your business',
'woocommerce'
) }
/>
),
progress: 50,
},
{
title: __( 'Assembling your AI-optimized store', 'woocommerce' ),
image: (
<img
src={ assemblingAiOptimizedStore }
alt={ __(
'Assembling your AI-optimized store',
'woocommerce'
) }
/>
),
progress: 66,
},
{
title: __( 'Applying the finishing touches', 'woocommerce' ),
image: (
<img
src={ applyingFinishingTouches }
alt={ __( 'Applying the finishing touches', 'woocommerce' ) }
/>
),
progress: 83,
},
];
export const ApiCallLoader = () => {
return (
<div>
<h1>Loader</h1>
<div>{ JSON.stringify( context ) }</div>
</div>
<Loader>
<Loader.Sequence interval={ 3000 }>
{ loaderSteps.map( ( step, index ) => (
<Loader.Layout key={ index }>
<Loader.Illustration>
{ step.image }
</Loader.Illustration>
<Loader.Title>{ step.title }</Loader.Title>
<Loader.ProgressBar progress={ step.progress || 0 } />
</Loader.Layout>
) ) }
</Loader.Sequence>
</Loader>
);
};

View File

@ -4,6 +4,7 @@
import { __experimentalRequestJetpackToken as requestJetpackToken } from '@woocommerce/ai';
import apiFetch from '@wordpress/api-fetch';
import { recordEvent } from '@woocommerce/tracks';
import { Sender } from 'xstate';
/**
* Internal dependencies
@ -101,6 +102,18 @@ export const getLookAndTone = async (
return parseLookAndToneCompletionResponse( data );
};
const browserPopstateHandler =
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
const popstateHandler = () => {
sendBack( { type: 'EXTERNAL_URL_UPDATE' } );
};
window.addEventListener( 'popstate', popstateHandler );
return () => {
window.removeEventListener( 'popstate', popstateHandler );
};
};
export const services = {
getLookAndTone,
browserPopstateHandler,
};

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { createMachine, sendParent } from 'xstate';
import { getQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -18,6 +19,20 @@ import {
} from './pages';
import { actions } from './actions';
import { services } from './services';
export const hasStepInUrl = (
_ctx: unknown,
_evt: unknown,
{ cond }: { cond: unknown }
) => {
const { path = '' } = getQuery() as { path: string };
const pathFragments = path.split( '/' );
return (
pathFragments[ 3 ] === // [0] '', [1] 'customize-store', [2] cys step slug [3] design-with-ai step slug
( cond as { step: string | undefined } ).step
);
};
export const designWithAiStateMachineDefinition = createMachine(
{
id: 'designWithAi',
@ -27,6 +42,17 @@ export const designWithAiStateMachineDefinition = createMachine(
context: {} as designWithAiStateMachineContext,
events: {} as designWithAiStateMachineEvents,
},
invoke: {
src: 'browserPopstateHandler',
},
on: {
EXTERNAL_URL_UPDATE: {
target: 'navigate',
},
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
actions: sendParent( ( _context, event ) => event ),
},
},
context: {
businessInfoDescription: {
descriptionText: '',
@ -39,8 +65,43 @@ export const designWithAiStateMachineDefinition = createMachine(
choice: '',
},
},
initial: 'businessInfoDescription',
initial: 'navigate',
states: {
navigate: {
always: [
{
target: 'businessInfoDescription',
cond: {
type: 'hasStepInUrl',
step: 'business-info-description',
},
},
{
target: 'lookAndFeel',
cond: {
type: 'hasStepInUrl',
step: 'look-and-feel',
},
},
{
target: 'toneOfVoice',
cond: {
type: 'hasStepInUrl',
step: 'tone-of-voice',
},
},
{
target: 'apiCallLoader',
cond: {
type: 'hasStepInUrl',
step: 'api-call-loader',
},
},
{
target: 'businessInfoDescription',
},
],
},
businessInfoDescription: {
id: 'businessInfoDescription',
initial: 'preBusinessInfoDescription',
@ -90,6 +151,12 @@ export const designWithAiStateMachineDefinition = createMachine(
meta: {
component: LookAndFeel,
},
entry: [
{
type: 'updateQueryStep',
step: 'look-and-feel',
},
],
on: {
LOOK_AND_FEEL_COMPLETE: {
actions: [ 'assignLookAndFeel' ],
@ -117,6 +184,12 @@ export const designWithAiStateMachineDefinition = createMachine(
meta: {
component: ToneOfVoice,
},
entry: [
{
type: 'updateQueryStep',
step: 'tone-of-voice',
},
],
on: {
TONE_OF_VOICE_COMPLETE: {
actions: [ 'assignToneOfVoice' ],
@ -144,19 +217,23 @@ export const designWithAiStateMachineDefinition = createMachine(
meta: {
component: ApiCallLoader,
},
entry: [
{
type: 'updateQueryStep',
step: 'api-call-loader',
},
],
},
postApiCallLoader: {},
},
},
},
on: {
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
actions: sendParent( ( _context, event ) => event ),
},
},
},
{
actions,
services,
guards: {
hasStepInUrl,
},
}
);

View File

@ -1,13 +1,10 @@
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
import { ApiCallLoader } from '../pages';
import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout';
export const ApiCallLoaderPage = () => (
<ApiCallLoader context={ {} as designWithAiStateMachineContext } />
);
export const ApiCallLoaderPage = () => <ApiCallLoader />;
export default {
title: 'WooCommerce Admin/Application/Customize Store/Design with AI/API Call Loader',

View File

@ -7,7 +7,13 @@ import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout';
export const BusinessInfoDescriptionPage = () => (
<BusinessInfoDescription
context={ {} as designWithAiStateMachineContext }
context={
{
businessInfoDescription: {
descriptionText: '',
},
} as designWithAiStateMachineContext
}
sendEvent={ () => {} }
/>
);

View File

@ -7,7 +7,13 @@ import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout';
export const LookAndFeelPage = () => (
<LookAndFeel
context={ {} as designWithAiStateMachineContext }
context={
{
lookAndFeel: {
choice: '',
},
} as designWithAiStateMachineContext
}
sendEvent={ () => {} }
/>
);

View File

@ -7,7 +7,13 @@ import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout';
export const ToneOfVoicePage = () => (
<ToneOfVoice
context={ {} as designWithAiStateMachineContext }
context={
{
toneOfVoice: {
choice: '',
},
} as designWithAiStateMachineContext
}
sendEvent={ () => {} }
/>
);

View File

@ -24,10 +24,7 @@ export type designWithAiStateMachineEvents =
type: 'TONE_OF_VOICE_COMPLETE';
}
| {
type: 'API_CALL_TO_AI_SUCCCESSFUL';
}
| {
type: 'API_CALL_TO_AI_FAILED';
type: 'EXTERNAL_URL_UPDATE';
};
export const VALID_LOOKS = [ 'Contemporary', 'Classic', 'Bold' ] as const;

View File

@ -1,9 +1,10 @@
/**
* External dependencies
*/
import { createMachine } from 'xstate';
import { Sender, createMachine } from 'xstate';
import { useEffect, useMemo, useState } from '@wordpress/element';
import { useMachine, useSelector } from '@xstate/react';
import { getQuery, updateQueryString } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -30,18 +31,53 @@ export type customizeStoreStateMachineEvents =
| introEvents
| designWithAiEvents
| assemblerHubEvents
| { type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION'; payload: { step: string } };
| { type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION'; payload: { step: string } }
| { type: 'EXTERNAL_URL_UPDATE' };
export const customizeStoreStateMachineServices = {
...introServices,
const updateQueryStep = (
_context: unknown,
_evt: unknown,
{ action }: { action: unknown }
) => {
const { path } = getQuery() as { path: string };
const step = ( action as { step: string } ).step;
const pathFragments = path.split( '/' ); // [0] '', [1] 'customize-store', [2] step slug [3] design-with-ai, assembler-hub path fragments
if ( pathFragments[ 1 ] === 'customize-store' ) {
if ( pathFragments[ 2 ] !== step ) {
// this state machine is only concerned with [2], so we ignore changes to [3]
// [1] is handled by router at root of wc-admin
updateQueryString( {}, `/customize-store/${ step }` );
}
}
};
const browserPopstateHandler =
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
const popstateHandler = () => {
sendBack( { type: 'EXTERNAL_URL_UPDATE' } );
};
window.addEventListener( 'popstate', popstateHandler );
return () => {
window.removeEventListener( 'popstate', popstateHandler );
};
};
export const machineActions = {
updateQueryStep,
};
export const customizeStoreStateMachineActions = {
...introActions,
...machineActions,
};
export const customizeStoreStateMachineServices = {
...introServices,
browserPopstateHandler,
};
export const customizeStoreStateMachineDefinition = createMachine( {
id: 'customizeStore',
initial: 'intro',
initial: 'navigate',
predictableActionArguments: true,
preserveActionOrder: true,
schema: {
@ -57,7 +93,47 @@ export const customizeStoreStateMachineDefinition = createMachine( {
activeTheme: '',
},
} as customizeStoreStateMachineContext,
invoke: {
src: 'browserPopstateHandler',
},
on: {
EXTERNAL_URL_UPDATE: {
target: 'navigate',
},
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
target: 'intro',
actions: [ { type: 'updateQueryStep', step: 'intro' } ],
},
},
states: {
navigate: {
always: [
{
target: 'intro',
cond: {
type: 'hasStepInUrl',
step: 'intro',
},
},
{
target: 'designWithAi',
cond: {
type: 'hasStepInUrl',
step: 'design-with-ai',
},
},
{
target: 'assemblerHub',
cond: {
type: 'hasStepInUrl',
step: 'assembler-hub',
},
},
{
target: 'intro',
},
],
},
intro: {
id: 'intro',
initial: 'preIntro',
@ -107,6 +183,9 @@ export const customizeStoreStateMachineDefinition = createMachine( {
meta: {
component: DesignWithAi,
},
entry: [
{ type: 'updateQueryStep', step: 'design-with-ai' },
],
},
},
on: {
@ -116,8 +195,16 @@ export const customizeStoreStateMachineDefinition = createMachine( {
},
},
assemblerHub: {
meta: {
component: AssemblerHub,
initial: 'assemblerHub',
states: {
assemblerHub: {
entry: [
{ type: 'updateQueryStep', step: 'assembler-hub' },
],
meta: {
component: AssemblerHub,
},
},
},
on: {
FINISH_CUSTOMIZATION: {
@ -131,13 +218,6 @@ export const customizeStoreStateMachineDefinition = createMachine( {
backToHomescreen: {},
appearanceTask: {},
},
on: {
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
actions: () => {
// TODO: when navigation has been implemented, this should navigate back to the Intro screen
},
},
},
} );
export const CustomizeStoreController = ( {
@ -159,7 +239,16 @@ export const CustomizeStoreController = ( {
...customizeStoreStateMachineActions,
...actionOverrides,
},
guards: {},
guards: {
hasStepInUrl: ( _ctx, _evt, { cond }: { cond: unknown } ) => {
const { path = '' } = getQuery() as { path: string };
const pathFragments = path.split( '/' );
return (
pathFragments[ 2 ] === // [0] '', [1] 'customize-store', [2] step slug
( cond as { step: string | undefined } ).step
);
},
},
} );
}, [ actionOverrides, servicesOverrides ] );

View File

@ -16,6 +16,7 @@ import ProductLoader from '../product-loader/product-loader';
import NoResults from '../product-list-content/no-results';
import { Product, SearchAPIProductType } from '../product-list/types';
import { MARKETPLACE_SEARCH_API_PATH, MARKETPLACE_HOST } from '../constants';
import { getAdminSetting } from '../../../utils/admin-settings';
export default function Extensions(): JSX.Element {
const [ productList, setProductList ] = useState< Product[] >( [] );
@ -38,6 +39,11 @@ export default function Extensions(): JSX.Element {
params.append( 'category', query.category );
}
const wccomSettings = getAdminSetting( 'wccomHelper', false );
if ( wccomSettings.storeCountry ) {
params.append( 'country', wccomSettings.storeCountry );
}
const wccomSearchEndpoint =
MARKETPLACE_HOST +
MARKETPLACE_SEARCH_API_PATH +

View File

@ -1,33 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
export default function HeaderSearchButton() {
return (
<button className="woocommerce-marketplace__header-search-button">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="Union"
fillRule="evenodd"
clipRule="evenodd"
d="M19.0001 11C19.0001 14.3137 16.3138 17 13.0001 17C11.6135 17 10.3369 16.5297 9.32086 15.7399L5.53039 19.5304L4.46973 18.4697L8.26019 14.6793C7.47038 13.6632 7.00006 12.3865 7.00006 11C7.00006 7.68629 9.68635 5 13.0001 5C16.3138 5 19.0001 7.68629 19.0001 11ZM17.5001 11C17.5001 13.4853 15.4853 15.5 13.0001 15.5C10.5148 15.5 8.50006 13.4853 8.50006 11C8.50006 8.51472 10.5148 6.5 13.0001 6.5C15.4853 6.5 17.5001 8.51472 17.5001 11Z"
fill="#1E1E1E"
/>
</svg>
<span className="screen-reader-text">
{ __( 'Search', 'woocommerce' ) }
</span>
</button>
);
}

View File

@ -1,39 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
export default function HeaderSearch() {
return (
<form className="woocommerce-marketplace__header-search">
<input
type="search"
className="woocommerce-marketplace__header-search-field"
placeholder={ __(
'Search extensions and themes',
'woocommerce'
) }
/>
<button className="woocommerce-marketplace__header-search-button">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="Union"
fillRule="evenodd"
clipRule="evenodd"
d="M19.0001 11C19.0001 14.3137 16.3138 17 13.0001 17C11.6135 17 10.3369 16.5297 9.32086 15.7399L5.53039 19.5304L4.46973 18.4697L8.26019 14.6793C7.47038 13.6632 7.00006 12.3865 7.00006 11C7.00006 7.68629 9.68635 5 13.0001 5C16.3138 5 19.0001 7.68629 19.0001 11ZM17.5001 11C17.5001 13.4853 15.4853 15.5 13.0001 15.5C10.5148 15.5 8.50006 13.4853 8.50006 11C8.50006 8.51472 10.5148 6.5 13.0001 6.5C15.4853 6.5 17.5001 8.51472 17.5001 11Z"
fill="#1E1E1E"
/>
</svg>
<span className="screen-reader-text">
{ __( 'Search', 'woocommerce' ) }
</span>
</button>
</form>
);
}

View File

@ -20,6 +20,11 @@
gap: $large-gap;
grid-template-columns: repeat(2, 1fr);
}
// Hide third and above product cards on Discover page due to API result count
// These are progressively displayed at larger screen sizes.
&__discover .woocommerce-marketplace__product-card:nth-child(n+3) {
display: none;
}
}
}
@ -29,6 +34,9 @@
gap: $large-gap;
grid-template-columns: repeat(3, 1fr);
}
&__discover .woocommerce-marketplace__product-card:nth-child(3) {
display: block;
}
}
}
@ -37,5 +45,8 @@
&__product-list-content {
grid-template-columns: repeat(4, 1fr);
}
&__discover .woocommerce-marketplace__product-card:nth-child(4) {
display: block;
}
}
}

View File

@ -4,6 +4,7 @@
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { TaskType } from '@woocommerce/data';
import { getAdminLink } from '@woocommerce/settings';
/**
* Internal dependencies
@ -12,7 +13,6 @@ import { WC_ASSET_URL } from '../../../../utils/admin-settings';
const CustomizeStoreHeader = ( {
task,
goToTask,
}: {
task: TaskType;
goToTask: React.MouseEventHandler;
@ -40,7 +40,12 @@ const CustomizeStoreHeader = ( {
<Button
isSecondary={ task.isComplete }
isPrimary={ ! task.isComplete }
onClick={ goToTask }
onClick={ () => {
// We need to use window.location.href instead of navigateTo because we need to initiate a full page refresh to ensure that all dependencies are loaded.
window.location.href = getAdminLink(
'admin.php?page=wc-admin&path=%2Fcustomize-store'
);
} }
>
{ __( 'Start customizing', 'woocommerce' ) }
</Button>

View File

@ -217,12 +217,6 @@ const webpackConfig = {
return null;
}
if ( request === '@wordpress/router' ) {
// The external wp.router does not exist in WP 6.2 and below, so we need to skip requesting to external here.
// We use the router in the customize store. We can remove this once our minimum support is WP 6.3.
return null;
}
if ( request.startsWith( '@wordpress/edit-site' ) ) {
// The external wp.editSite does not include edit-site components, so we need to skip requesting to external here. We can remove this once the edit-site components are exported in the external wp.editSite.
// We use the edit-site components in the customize store.

View File

@ -10,7 +10,6 @@
/tests/
/e2e/
babel.config.js
changelog.txt
composer.*
contributors.html
docker-compose.yaml

Some files were not shown because too many files have changed in this diff Show More