| @@ -8,8 +8,10 @@ | ||
| 8 | 8 | use FluentCart\App\Helpers\AddressHelper; |
| 9 | 9 | use FluentCart\App\Helpers\CurrenciesHelper; |
| 10 | 10 | use FluentCart\App\Services\OrderService; |
| 11 | 11 | use FluentCart\App\Services\Theme\ColorPalette; |
| 12 | +use FluentCart\App\Services\Theme\FrontendTheme; | |
| 13 | +use FluentCart\App\Services\Theme\RadiusPalette; | |
| 12 | 14 | use FluentCart\App\Services\Theme\ThemePalette; |
| 13 | 15 | use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager; |
| 14 | 16 | use FluentCart\App\Modules\StoreManagedRenewal\Services\RenewalService; |
| 15 | 17 | use FluentCart\App\Modules\Subscriptions\Services\SubscriptionManagementMode; |
| @@ -82,9 +84,10 @@ | ||
| 82 | 84 | 'show_cart_icon_in_body' => 'yes', |
| 83 | 85 | 'additional_address_field' => 'yes', |
| 84 | 86 | 'hide_coupon_field' => 'no', |
| 85 | 87 | 'user_account_creation_mode' => 'all', |
| 86 | - 'auto_login_after_account_creation' => 'no', | |
| 88 | + 'auto_login_after_account_creation' => 'yes', | |
| 89 | + 'require_customer_email_verification' => 'no', | |
| 87 | 90 | 'checkout_page_id' => '', |
| 88 | 91 | 'custom_payment_page_id' => '', |
| 89 | 92 | 'registration_page_id' => '', |
| 90 | 93 | 'login_page_id' => '', |
| @@ -89,8 +92,9 @@ | ||
| 89 | 92 | 'registration_page_id' => '', |
| 90 | 93 | 'login_page_id' => '', |
| 91 | 94 | 'cart_page_id' => '', |
| 92 | 95 | 'receipt_page_id' => '', |
| 96 | + 'order_review_page_id' => '', | |
| 93 | 97 | 'shop_page_id' => '', |
| 94 | 98 | 'customer_profile_page_id' => '', |
| 95 | 99 | 'customer_profile_page_slug' => '', |
| 96 | 100 | 'currency' => 'USD', |
| @@ -101,8 +105,11 @@ | ||
| 101 | 105 | 'store_postcode' => '', |
| 102 | 106 | 'store_state' => '', |
| 103 | 107 | 'show_relevant_product_in_single_page' => 'yes', |
| 104 | 108 | 'show_relevant_product_in_modal' => '', |
| 109 | + 'show_reviews_in_single_page' => 'yes', | |
| 110 | + 'show_rating_in_shop' => 'yes', | |
| 111 | + 'show_rating_in_relevant' => 'yes', | |
| 105 | 112 | 'order_mode' => 'test', |
| 106 | 113 | 'subscription_mode_guard' => 'yes', |
| 107 | 114 | 'variation_view' => 'both', |
| 108 | 115 | 'variation_columns' => 'masonry', |
| @@ -115,8 +122,9 @@ | ||
| 115 | 122 | 'weight_unit' => 'kg', |
| 116 | 123 | 'dimension_unit' => 'cm', |
| 117 | 124 | 'appearance_source' => ColorPalette::SOURCE_DEFAULT, |
| 118 | 125 | 'appearance_colors' => [], |
| 126 | + 'appearance_radius' => [], | |
| 119 | 127 | // 'wordpress', not 'fluent_cart': the FluentCart patterns are |
| 120 | 128 | // literals ('M j, Y'), and a literal renders a half-translated date |
| 121 | 129 | // on a localized store -- a German month in English field order, |
| 122 | 130 | // which a German reader misreads as day-first. Following |
| @@ -143,9 +151,10 @@ | ||
| 143 | 151 | 'shop_page_id' => $this->getShopPage(), |
| 144 | 152 | 'customer_profile_page_id' => $this->getCustomerProfilePage(), |
| 145 | 153 | 'cart_page_id' => $this->getCartPage(), |
| 146 | 154 | 'checkout_page_id' => $this->getCheckoutPage(), |
| 147 | - 'receipt_page_id' => $this->getReceiptPage() | |
| 155 | + 'receipt_page_id' => $this->getReceiptPage(), | |
| 156 | + 'order_review_page_id' => $this->getOrderReviewPage() | |
| 148 | 157 | ]; |
| 149 | 158 | $isProActive = App::isProActive(); |
| 150 | 159 | $proFeatureIcon = Vite::getAssetUrl('images/crown.svg'); |
| 151 | 160 | |
| @@ -892,8 +901,45 @@ | ||
| 892 | 901 | ], |
| 893 | 902 | ] |
| 894 | 903 | ], |
| 895 | 904 | |
| 905 | + 'hr_order_review' => [ | |
| 906 | + 'type' => 'html', | |
| 907 | + 'value' => '<hr class="settings-divider">' | |
| 908 | + ], | |
| 909 | + | |
| 910 | + 'order_review_page_grid' => [ | |
| 911 | + 'type' => 'grid', | |
| 912 | + 'columns' => [ | |
| 913 | + 'default' => 1, | |
| 914 | + 'md' => 3 | |
| 915 | + ], | |
| 916 | + 'disable_nesting' => true, | |
| 917 | + 'schema' => [ | |
| 918 | + 'label' => [ | |
| 919 | + 'type' => 'html', | |
| 920 | + 'value' => '<span class="setting-label">' . __('Select Order Review Page', 'fluent-cart') . '</span> | |
| 921 | + <div class="form-note">' . __("Select the page where customers will review the products they bought, from the link in their order email.", 'fluent-cart') . '</div>' | |
| 922 | + ], | |
| 923 | + 'order_review_page_id' => [ | |
| 924 | + 'wrapperClass' => 'col-span-2', | |
| 925 | + 'label' => false, | |
| 926 | + 'page_title' => __('Review Your Order', 'fluent-cart'), | |
| 927 | + 'type' => 'component', | |
| 928 | + 'component' => 'StoreSettings/PageSelector', | |
| 929 | + 'page_key' => 'order_review_page_id', | |
| 930 | + 'preview_link' => $previewLinks['order_review_page_id'], | |
| 931 | + 'hide_note' => true, | |
| 932 | + 'options' => $pages, | |
| 933 | + 'value' => '', | |
| 934 | + 'note' => \FluentCart\App\Helpers\Helper::getShortcodeInstructionString( | |
| 935 | + '[fluent_cart_order_review]', | |
| 936 | + __('Review Your Order', 'fluent-cart') | |
| 937 | + ), | |
| 938 | + ], | |
| 939 | + ] | |
| 940 | + ], | |
| 941 | + | |
| 896 | 942 | 'hr4' => [ |
| 897 | 943 | 'type' => 'html', |
| 898 | 944 | 'value' => '<hr class="settings-divider">' |
| 899 | 945 | ], |
| @@ -951,9 +997,9 @@ | ||
| 951 | 997 | 'schema' => [ |
| 952 | 998 | 'label' => [ |
| 953 | 999 | 'type' => 'html', |
| 954 | 1000 | 'value' => '<span class="setting-label">' . __('Single Product Setup', 'fluent-cart') . '</span> |
| 955 | - <div class="form-note">' . __("Control the display of relevant information.", 'fluent-cart') . '</div>' | |
| 1001 | + <div class="form-note">' . __("Choose which sections to display on the single product page.", 'fluent-cart') . '</div>' | |
| 956 | 1002 | ], |
| 957 | 1003 | 'fields' => [ |
| 958 | 1004 | 'type' => 'grid', |
| 959 | 1005 | 'columns' => [ |
| @@ -971,8 +1017,17 @@ | ||
| 971 | 1017 | "label" => __('Show Relevant In Product Modal', 'fluent-cart'), |
| 972 | 1018 | "type" => "checkbox", |
| 973 | 1019 | "value" => "no" |
| 974 | 1020 | ], |
| 1021 | + "show_reviews_in_single_page" => [ | |
| 1022 | + "label" => __('Show Reviews In Single Page', 'fluent-cart'), | |
| 1023 | + "type" => "checkbox", | |
| 1024 | + "value" => "yes" | |
| 1025 | + ], | |
| 1026 | + "show_reviews_note" => [ | |
| 1027 | + "type" => "html", | |
| 1028 | + "value" => '<div class="form-note">' . __('For block themes, add the Product Reviews block to your Single Product template to display reviews on your product page.', 'fluent-cart') . '</div>' | |
| 1029 | + ], | |
| 975 | 1030 | ] |
| 976 | 1031 | ] |
| 977 | 1032 | |
| 978 | 1033 | ] |
| @@ -1134,8 +1189,53 @@ | ||
| 1134 | 1189 | ], |
| 1135 | 1190 | |
| 1136 | 1191 | ] |
| 1137 | 1192 | ], |
| 1193 | + | |
| 1194 | + 'hr_product_slug' => [ | |
| 1195 | + 'type' => 'html', | |
| 1196 | + 'value' => '<hr class="settings-divider">' | |
| 1197 | + ], | |
| 1198 | + | |
| 1199 | + 'product_rating_grid' => [ | |
| 1200 | + 'type' => 'grid', | |
| 1201 | + 'columns' => [ | |
| 1202 | + 'default' => 1, | |
| 1203 | + 'md' => 3 | |
| 1204 | + ], | |
| 1205 | + 'disable_nesting' => true, | |
| 1206 | + 'schema' => [ | |
| 1207 | + 'label' => [ | |
| 1208 | + 'type' => 'html', | |
| 1209 | + 'value' => sprintf( | |
| 1210 | + '<span class="setting-label">%1$s</span><div class="form-note">%2$s</div>', | |
| 1211 | + __('Product Rating', 'fluent-cart'), | |
| 1212 | + __('Show star ratings when reviews are enabled.', 'fluent-cart') | |
| 1213 | + ) | |
| 1214 | + ], | |
| 1215 | + 'fields' => [ | |
| 1216 | + 'type' => 'grid', | |
| 1217 | + 'columns' => [ | |
| 1218 | + 'default' => 1, | |
| 1219 | + 'md' => 1 | |
| 1220 | + ], | |
| 1221 | + 'disable_nesting' => true, | |
| 1222 | + 'schema' => [ | |
| 1223 | + "show_rating_in_shop" => [ | |
| 1224 | + "label" => __('Show Rating in Shop', 'fluent-cart'), | |
| 1225 | + "type" => "checkbox", | |
| 1226 | + "value" => "yes" | |
| 1227 | + ], | |
| 1228 | + "show_rating_in_relevant" => [ | |
| 1229 | + "label" => __('Show Rating in Relevant Products', 'fluent-cart'), | |
| 1230 | + "type" => "checkbox", | |
| 1231 | + "value" => "yes" | |
| 1232 | + ], | |
| 1233 | + ] | |
| 1234 | + ] | |
| 1235 | + | |
| 1236 | + ] | |
| 1237 | + ], | |
| 1138 | 1238 | ], |
| 1139 | 1239 | ], |
| 1140 | 1240 | 'compliance' => [ |
| 1141 | 1241 | 'title' => __('Compliance', 'fluent-cart'), |
| @@ -1146,27 +1246,99 @@ | ||
| 1146 | 1246 | 'default' => 1, |
| 1147 | 1247 | 'md' => 1, |
| 1148 | 1248 | ], |
| 1149 | 1249 | 'schema' => [ |
| 1150 | - 'auto_login_after_account_creation' => [ | |
| 1151 | - 'wrapperClass' => 'fct-compliance-auto-login', | |
| 1152 | - 'label' => __('Login after account creation', 'fluent-cart'), | |
| 1153 | - 'type' => 'radio', | |
| 1154 | - 'value' => 'no', | |
| 1155 | - 'attributes' => [ | |
| 1156 | - 'aria-label' => __('Login after account creation', 'fluent-cart'), | |
| 1250 | + 'email_verification_grid' => [ | |
| 1251 | + 'type' => 'grid', | |
| 1252 | + 'disable_nesting' => true, | |
| 1253 | + 'columns' => ['default' => 1, 'md' => 3], | |
| 1254 | + 'wrapperClass' => 'items-start', | |
| 1255 | + 'schema' => [ | |
| 1256 | + 'label' => [ | |
| 1257 | + 'type' => 'html', | |
| 1258 | + 'value' => sprintf( | |
| 1259 | + '<span class="setting-label">%1$s</span><div class="form-note">%2$s</div>', | |
| 1260 | + esc_html__('Customer email verification', 'fluent-cart'), | |
| 1261 | + esc_html__('Confirm email ownership before customers access their portal and saved checkout addresses.', 'fluent-cart') | |
| 1262 | + ), | |
| 1263 | + ], | |
| 1264 | + 'fields' => [ | |
| 1265 | + 'type' => 'grid', | |
| 1266 | + 'disable_nesting' => true, | |
| 1267 | + 'columns' => ['default' => 1], | |
| 1268 | + 'wrapperClass' => 'md:col-span-2', | |
| 1269 | + 'schema' => [ | |
| 1270 | + 'require_customer_email_verification' => [ | |
| 1271 | + 'wrapperClass' => 'fct-compliance-auto-login', | |
| 1272 | + 'label' => false, | |
| 1273 | + 'type' => 'radio', | |
| 1274 | + 'value' => 'no', | |
| 1275 | + 'attributes' => [ | |
| 1276 | + 'aria-label' => __('Customer email verification', 'fluent-cart'), | |
| 1277 | + ], | |
| 1278 | + 'options' => [ | |
| 1279 | + [ | |
| 1280 | + 'label' => __('Required', 'fluent-cart'), | |
| 1281 | + 'value' => 'yes', | |
| 1282 | + ], | |
| 1283 | + [ | |
| 1284 | + 'label' => __('Not required', 'fluent-cart'), | |
| 1285 | + 'value' => 'no', | |
| 1286 | + ], | |
| 1287 | + ], | |
| 1288 | + 'note' => __('When not required, new checkout accounts are linked to their purchases immediately.', 'fluent-cart'), | |
| 1289 | + ], | |
| 1290 | + ], | |
| 1291 | + ], | |
| 1157 | 1292 | ], |
| 1158 | - 'options' => [ | |
| 1159 | - [ | |
| 1160 | - 'label' => __("Don't auto login after account creation", 'fluent-cart'), | |
| 1161 | - 'value' => 'no', | |
| 1293 | + ], | |
| 1294 | + 'compliance_divider' => [ | |
| 1295 | + 'type' => 'html', | |
| 1296 | + 'value' => '<hr class="settings-divider">', | |
| 1297 | + ], | |
| 1298 | + 'account_login_grid' => [ | |
| 1299 | + 'type' => 'grid', | |
| 1300 | + 'disable_nesting' => true, | |
| 1301 | + 'columns' => ['default' => 1, 'md' => 3], | |
| 1302 | + 'wrapperClass' => 'items-start', | |
| 1303 | + 'schema' => [ | |
| 1304 | + 'label' => [ | |
| 1305 | + 'type' => 'html', | |
| 1306 | + 'value' => sprintf( | |
| 1307 | + '<span class="setting-label">%1$s</span><div class="form-note">%2$s</div>', | |
| 1308 | + esc_html__('Login after account creation', 'fluent-cart'), | |
| 1309 | + esc_html__('Choose how customers sign in after registering or completing checkout.', 'fluent-cart') | |
| 1310 | + ), | |
| 1162 | 1311 | ], |
| 1163 | - [ | |
| 1164 | - 'label' => __('Enable auto login after account creation', 'fluent-cart'), | |
| 1165 | - 'value' => 'yes', | |
| 1312 | + 'fields' => [ | |
| 1313 | + 'type' => 'grid', | |
| 1314 | + 'disable_nesting' => true, | |
| 1315 | + 'columns' => ['default' => 1], | |
| 1316 | + 'wrapperClass' => 'md:col-span-2', | |
| 1317 | + 'schema' => [ | |
| 1318 | + 'auto_login_after_account_creation' => [ | |
| 1319 | + 'wrapperClass' => 'fct-compliance-auto-login', | |
| 1320 | + 'label' => false, | |
| 1321 | + 'type' => 'radio', | |
| 1322 | + 'value' => 'yes', | |
| 1323 | + 'attributes' => [ | |
| 1324 | + 'aria-label' => __('Login after account creation', 'fluent-cart'), | |
| 1325 | + ], | |
| 1326 | + 'options' => [ | |
| 1327 | + [ | |
| 1328 | + 'label' => __('Log in automatically', 'fluent-cart'), | |
| 1329 | + 'value' => 'yes', | |
| 1330 | + ], | |
| 1331 | + [ | |
| 1332 | + 'label' => __('Let customers log in', 'fluent-cart'), | |
| 1333 | + 'value' => 'no', | |
| 1334 | + ], | |
| 1335 | + ], | |
| 1336 | + 'note' => __('Email verification still applies when required above.', 'fluent-cart'), | |
| 1337 | + ], | |
| 1338 | + ], | |
| 1166 | 1339 | ], |
| 1167 | 1340 | ], |
| 1168 | - 'note' => __('Choose whether customers are logged in automatically when FluentCart creates their account during registration or after checkout.', 'fluent-cart'), | |
| 1169 | 1341 | ], |
| 1170 | 1342 | ], |
| 1171 | 1343 | ], |
| 1172 | 1344 | 'cart_and_checkout' => [ |
| @@ -1542,8 +1714,25 @@ | ||
| 1542 | 1714 | ] |
| 1543 | 1715 | ]; |
| 1544 | 1716 | } |
| 1545 | 1717 | |
| 1718 | + // Review settings only apply while the reviews module is active — | |
| 1719 | + // hide them (and their dividers) otherwise: the Product Rating | |
| 1720 | + // section, the "Show Reviews In Single Page" toggle with its note, | |
| 1721 | + // and the Order Review page selector. The stored values survive: | |
| 1722 | + // save() merges the submitted form over what is stored, so a field | |
| 1723 | + // left off the form keeps its value for when the module returns. | |
| 1724 | + if (!ModuleSettings::isActive('reviews')) { | |
| 1725 | + unset( | |
| 1726 | + $fields['setting_tabs']['schema']['single_product_setup']['schema']['hr_product_slug'], | |
| 1727 | + $fields['setting_tabs']['schema']['single_product_setup']['schema']['product_rating_grid'], | |
| 1728 | + $fields['setting_tabs']['schema']['single_product_setup']['schema']['product_settings_grid']['schema']['fields']['schema']['show_reviews_in_single_page'], | |
| 1729 | + $fields['setting_tabs']['schema']['single_product_setup']['schema']['product_settings_grid']['schema']['fields']['schema']['show_reviews_note'], | |
| 1730 | + $fields['setting_tabs']['schema']['pages_setup']['schema']['hr_order_review'], | |
| 1731 | + $fields['setting_tabs']['schema']['pages_setup']['schema']['order_review_page_grid'] | |
| 1732 | + ); | |
| 1733 | + } | |
| 1734 | + | |
| 1546 | 1735 | return apply_filters("fluent_cart/store_settings/fields", $fields, []); |
| 1547 | 1736 | } |
| 1548 | 1737 | |
| 1549 | 1738 | public function isModuleTabEnabled(): bool |
| @@ -1573,8 +1762,28 @@ | ||
| 1573 | 1762 | |
| 1574 | 1763 | return empty($data) ? $default : $data; |
| 1575 | 1764 | } |
| 1576 | 1765 | |
| 1766 | + /** | |
| 1767 | + * All store settings, as the settings form should edit them. | |
| 1768 | + * | |
| 1769 | + * A stored radius the grammar refuses (hand-edited, or written by an older | |
| 1770 | + * build) never printed on the storefront; dropped here too, so the form | |
| 1771 | + * never holds a value it would refuse to save. | |
| 1772 | + * | |
| 1773 | + * @return array | |
| 1774 | + */ | |
| 1775 | + public function getForEditing(): array | |
| 1776 | + { | |
| 1777 | + $settings = (array)$this->get(); | |
| 1778 | + | |
| 1779 | + if (array_key_exists('appearance_radius', $settings)) { | |
| 1780 | + $settings['appearance_radius'] = RadiusPalette::sanitizeOwnerMap($settings['appearance_radius']); | |
| 1781 | + } | |
| 1782 | + | |
| 1783 | + return $settings; | |
| 1784 | + } | |
| 1785 | + | |
| 1577 | 1786 | public function moduleSettings(): array |
| 1578 | 1787 | { |
| 1579 | 1788 | $settings = $this->get('modules_settings', []); |
| 1580 | 1789 | return Arr::wrap($settings); |
| @@ -1657,8 +1866,9 @@ | ||
| 1657 | 1866 | 'registration_page_id', |
| 1658 | 1867 | 'login_page_id', |
| 1659 | 1868 | 'cart_page_id', |
| 1660 | 1869 | 'receipt_page_id', |
| 1870 | + 'order_review_page_id', | |
| 1661 | 1871 | 'shop_page_id', |
| 1662 | 1872 | 'customer_profile_page_id', |
| 1663 | 1873 | 'customer_profile_page_slug', |
| 1664 | 1874 | ] |
| @@ -1734,9 +1944,29 @@ | ||
| 1734 | 1944 | } |
| 1735 | 1945 | return home_url(); |
| 1736 | 1946 | } |
| 1737 | 1947 | |
| 1948 | + public function getOrderReviewPageId() | |
| 1949 | + { | |
| 1950 | + return Arr::get($this->storeSettings, 'order_review_page_id'); | |
| 1951 | + } | |
| 1738 | 1952 | |
| 1953 | + /** | |
| 1954 | + * The page hosting [fluent_cart_order_review], or home_url() when the store | |
| 1955 | + * has not picked one — Order::getOrderReviewUrl() then falls back to the | |
| 1956 | + * ?fluent-cart=order-review route, which needs no page at all. | |
| 1957 | + */ | |
| 1958 | + public function getOrderReviewPage(): string | |
| 1959 | + { | |
| 1960 | + if ($pageId = $this->getOrderReviewPageId()) { | |
| 1961 | + if (Pages::isPage($pageId)) { | |
| 1962 | + return $this->getPageLink($pageId); | |
| 1963 | + } | |
| 1964 | + } | |
| 1965 | + return home_url(); | |
| 1966 | + } | |
| 1967 | + | |
| 1968 | + | |
| 1739 | 1969 | public function getBuyButtonText(): string |
| 1740 | 1970 | { |
| 1741 | 1971 | return esc_html(Arr::get($this->storeSettings, 'checkout_button_text', __('Buy now', 'fluent-cart'))); |
| 1742 | 1972 | } |
| @@ -1896,34 +2126,30 @@ | ||
| 1896 | 2126 | // Plain text — the component renders these as text nodes, so no markup |
| 1897 | 2127 | // and no escaping here or the entities would show up verbatim. |
| 1898 | 2128 | $sourceOptions = [ |
| 1899 | 2129 | [ |
| 1900 | - 'label' => __("FluentCart's own colors", 'fluent-cart'), | |
| 2130 | + 'label' => __("FluentCart's own style", 'fluent-cart'), | |
| 1901 | 2131 | 'value' => ColorPalette::SOURCE_DEFAULT, |
| 1902 | 2132 | 'icon' => 'PaletteLine', |
| 1903 | - 'note' => __('The storefront keeps the colors it ships with.', 'fluent-cart'), | |
| 2133 | + 'note' => __('The storefront keeps the colors and corner rounding it ships with.', 'fluent-cart'), | |
| 1904 | 2134 | ], |
| 1905 | 2135 | [ |
| 1906 | 2136 | 'label' => __('Inherit from the active theme', 'fluent-cart'), |
| 1907 | 2137 | 'value' => ColorPalette::SOURCE_THEME, |
| 1908 | 2138 | 'icon' => 'PaintLine', |
| 1909 | - 'note' => sprintf( | |
| 1910 | - '%1$s %2$s', | |
| 1911 | - __('The storefront palette is rebuilt from your theme, and follows it when the theme changes.', 'fluent-cart'), | |
| 1912 | - ThemePalette::sourceLabel() | |
| 1913 | - ), | |
| 2139 | + 'note' => __('FluentCart uses your active theme\'s colors, and its button and form field corner rounding, for the storefront. Product cards keep FluentCart\'s corners. If something doesn\'t come through as expected, choose Customize to set it yourself.', 'fluent-cart'), | |
| 1914 | 2140 | ], |
| 1915 | 2141 | [ |
| 1916 | 2142 | 'label' => __('Customize', 'fluent-cart'), |
| 1917 | 2143 | 'value' => ColorPalette::SOURCE_CUSTOM, |
| 1918 | 2144 | 'icon' => 'PaletteLine', |
| 1919 | - 'note' => __('Pick the colors yourself. Only the ones you set are written to the storefront.', 'fluent-cart'), | |
| 2145 | + 'note' => __('Pick the colors and corner rounding yourself. Only what you set is written to the storefront.', 'fluent-cart'), | |
| 1920 | 2146 | ], |
| 1921 | 2147 | ]; |
| 1922 | 2148 | |
| 1923 | 2149 | $sourceHeading = [ |
| 1924 | - 'label' => __('Where colors come from', 'fluent-cart'), | |
| 1925 | - 'note' => __('Storefront colors cascade from a small set of globals, so changing one here updates every page that uses it.', 'fluent-cart'), | |
| 2150 | + 'label' => __('Where colors and corner rounding come from', 'fluent-cart'), | |
| 2151 | + 'note' => __('Storefront colors and corner rounding cascade from a small set of globals, so changing one here updates every page that uses it.', 'fluent-cart'), | |
| 1926 | 2152 | ]; |
| 1927 | 2153 | |
| 1928 | 2154 | return [ |
| 1929 | 2155 | 'title' => __('Appearance', 'fluent-cart'), |
| @@ -1947,8 +2173,9 @@ | ||
| 1947 | 2173 | 'heading' => $sourceHeading, |
| 1948 | 2174 | 'source_options' => $sourceOptions, |
| 1949 | 2175 | 'custom_source' => ColorPalette::SOURCE_CUSTOM, |
| 1950 | 2176 | 'color_groups' => $this->getAppearanceColorGroups(), |
| 2177 | + 'radius_section' => $this->getAppearanceRadiusSection(), | |
| 1951 | 2178 | 'preview' => $this->getAppearancePreviewData(), |
| 1952 | 2179 | ], |
| 1953 | 2180 | 'appearance_source' => [ |
| 1954 | 2181 | 'type' => 'hidden', |
| @@ -1957,8 +2184,12 @@ | ||
| 1957 | 2184 | 'appearance_colors' => [ |
| 1958 | 2185 | 'type' => 'hidden', |
| 1959 | 2186 | 'value' => (object)[], |
| 1960 | 2187 | ], |
| 2188 | + 'appearance_radius' => [ | |
| 2189 | + 'type' => 'hidden', | |
| 2190 | + 'value' => (object)[], | |
| 2191 | + ], | |
| 1961 | 2192 | ], |
| 1962 | 2193 | ]; |
| 1963 | 2194 | } |
| 1964 | 2195 | |
| @@ -1983,8 +2214,9 @@ | ||
| 1983 | 2214 | return [ |
| 1984 | 2215 | 'slots' => $this->getAppearancePreviewSlots(), |
| 1985 | 2216 | 'theme_source' => ColorPalette::SOURCE_THEME, |
| 1986 | 2217 | 'theme_roles' => $this->getAppearanceThemeRoles(), |
| 2218 | + 'theme_radii' => $this->getAppearanceThemeRadii(), | |
| 1987 | 2219 | 'notes' => [ |
| 1988 | 2220 | ColorPalette::SOURCE_THEME => $this->getAppearanceThemeNote(), |
| 1989 | 2221 | ColorPalette::SOURCE_CUSTOM => __('Set only the colors you want to change — anything you leave unset keeps FluentCart\'s own default.', 'fluent-cart'), |
| 1990 | 2222 | ], |
| @@ -2047,13 +2279,16 @@ | ||
| 2047 | 2279 | */ |
| 2048 | 2280 | protected function getAppearanceThemeNote(): string |
| 2049 | 2281 | { |
| 2050 | 2282 | if (!ThemePalette::hasUsableSource()) { |
| 2051 | - return __('The active theme publishes nothing to inherit, so the storefront keeps FluentCart\'s own colors.', 'fluent-cart'); | |
| 2283 | + return __('The active theme publishes no colors to inherit, so the storefront keeps FluentCart\'s own colors.', 'fluent-cart'); | |
| 2052 | 2284 | } |
| 2053 | 2285 | |
| 2286 | + // Only a colour with no hex anywhere is unpreviewable. A live | |
| 2287 | + // reference with a hex fallback — what the theme-settings readers and | |
| 2288 | + // Blocksy write — previews as that fallback. | |
| 2054 | 2289 | foreach (ThemePalette::resolve() as $value) { |
| 2055 | - if (!sanitize_hex_color((string)$value)) { | |
| 2290 | + if ((string)$value !== '' && ThemePalette::measurable((string)$value) === '') { | |
| 2056 | 2291 | return __('This theme publishes its palette as CSS variables. The storefront reads them, but they cannot be resolved here — the preview shows FluentCart\'s colors in their place.', 'fluent-cart'); |
| 2057 | 2292 | } |
| 2058 | 2293 | } |
| 2059 | 2294 | |
| @@ -2079,13 +2314,15 @@ | ||
| 2079 | 2314 | |
| 2080 | 2315 | $roles = []; |
| 2081 | 2316 | |
| 2082 | 2317 | foreach (ThemePalette::resolve() as $role => $value) { |
| 2083 | - // A theme that publishes `var(--x)` is written to the storefront | |
| 2084 | - // verbatim and resolved by the browser there, but that property is | |
| 2085 | - // not declared in wp-admin. Unpreviewable, so leave it out and let | |
| 2086 | - // the slot fall back to its own default. | |
| 2087 | - $hex = sanitize_hex_color((string)$value); | |
| 2318 | + // A theme that publishes a bare `var(--x)` is written to the | |
| 2319 | + // storefront verbatim and resolved by the browser there, but that | |
| 2320 | + // property is not declared in wp-admin. Unpreviewable, so leave it | |
| 2321 | + // out and let the slot fall back to its own default. A reference | |
| 2322 | + // with a hex fallback (`var(--x, #hex)`) previews as that hex — | |
| 2323 | + // the same colour the storefront measures it by. | |
| 2324 | + $hex = ThemePalette::measurable((string)$value); | |
| 2088 | 2325 | |
| 2089 | 2326 | if ($hex) { |
| 2090 | 2327 | $roles[$role] = $hex; |
| 2091 | 2328 | } |
| @@ -2094,8 +2331,21 @@ | ||
| 2094 | 2331 | return $roles; |
| 2095 | 2332 | } |
| 2096 | 2333 | |
| 2097 | 2334 | /** |
| 2335 | + * The theme's border radii for the preview, role => length. | |
| 2336 | + * | |
| 2337 | + * Exactly what inheriting would print, storefront filter included, so the | |
| 2338 | + * preview never promises a radius the storefront won't write. | |
| 2339 | + * | |
| 2340 | + * @return array Role key => length (`8px`, `0.5rem`). | |
| 2341 | + */ | |
| 2342 | + protected function getAppearanceThemeRadii(): array | |
| 2343 | + { | |
| 2344 | + return FrontendTheme::radiiFor(ColorPalette::SOURCE_THEME); | |
| 2345 | + } | |
| 2346 | + | |
| 2347 | + /** | |
| 2098 | 2348 | * The colour knobs, grouped the way the registry groups them, as data the |
| 2099 | 2349 | * appearance component renders its pickers from. |
| 2100 | 2350 | * |
| 2101 | 2351 | * @return array |
| @@ -2129,8 +2379,38 @@ | ||
| 2129 | 2379 | ]; |
| 2130 | 2380 | } |
| 2131 | 2381 | |
| 2132 | 2382 | return $groups; |
| 2383 | + } | |
| 2384 | + | |
| 2385 | + /** | |
| 2386 | + * The border-radius controls, as data the appearance component renders | |
| 2387 | + * its number inputs from. | |
| 2388 | + * | |
| 2389 | + * Driven off RadiusPalette::roles(), the same registry the sanitiser and | |
| 2390 | + * the storefront CSS read, so a control can never write a role the page | |
| 2391 | + * would not print. | |
| 2392 | + * | |
| 2393 | + * @return array | |
| 2394 | + */ | |
| 2395 | + protected function getAppearanceRadiusSection(): array | |
| 2396 | + { | |
| 2397 | + $fields = []; | |
| 2398 | + | |
| 2399 | + foreach (RadiusPalette::roles() as $key => $definition) { | |
| 2400 | + $fields[] = [ | |
| 2401 | + 'key' => $key, | |
| 2402 | + 'label' => (string)Arr::get($definition, 'label', $key), | |
| 2403 | + 'note' => (string)Arr::get($definition, 'note', ''), | |
| 2404 | + ]; | |
| 2405 | + } | |
| 2406 | + | |
| 2407 | + return [ | |
| 2408 | + 'label' => __('Border radius', 'fluent-cart'), | |
| 2409 | + 'note' => __('A number is read as pixels, or add a unit: 8, 8px, 0.5rem, 1em. Leave a field empty to keep FluentCart\'s default.', 'fluent-cart'), | |
| 2410 | + 'invalid' => __('Use a number, optionally with px, rem or em.', 'fluent-cart'), | |
| 2411 | + 'fields' => $fields, | |
| 2412 | + ]; | |
| 2133 | 2413 | } |
| 2134 | 2414 | |
| 2135 | 2415 | /** |
| 2136 | 2416 | * The hint under one colour picker: what it drives, and the value it |