| @@ -5,9 +5,9 @@ | ||
| 5 | 5 | use FluentCart\Api\StoreSettings; |
| 6 | 6 | use FluentCart\Framework\Support\Arr; |
| 7 | 7 | |
| 8 | 8 | /** |
| 9 | - * Writes the storefront's global colour custom properties. | |
| 9 | + * Writes the storefront's global colour and border-radius custom properties. | |
| 10 | 10 | * |
| 11 | 11 | * FluentCart's stylesheets declare every scoped colour as |
| 12 | 12 | * `var(--fct-<global>, <fallback>)`, so the globals printed here cascade to the |
| 13 | 13 | * whole store — shop grid, product page, cart drawer, checkout and the customer |
| @@ -311,11 +311,13 @@ | ||
| 311 | 311 | } |
| 312 | 312 | |
| 313 | 313 | /* |
| 314 | 314 | * Not escaped on output because it cannot carry anything to escape: |
| 315 | - * every property name comes from the ColorPalette registry and every | |
| 316 | - * value has been through sanitize_hex_color(), so the string is only | |
| 317 | - * ever `--fct-name: #rrggbb;`. | |
| 315 | + * every property name comes from the ColorPalette or RadiusPalette | |
| 316 | + * registry, every colour value is a hex or a bare var() reference | |
| 317 | + * (sanitizeDeclarationValue()), and every radius value is one plain | |
| 318 | + * length (RadiusPalette::sanitizeLength()), so the string is only ever | |
| 319 | + * `--fct-name:#rrggbb;`, `--fct-name:var(--x);` or `--fct-name:8px;`. | |
| 318 | 320 | */ |
| 319 | 321 | echo '<style id="' . esc_attr(self::STYLE_ID) . '">' . $css . '</style>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 320 | 322 | } |
| 321 | 323 | |
| @@ -526,14 +528,148 @@ | ||
| 526 | 528 | return $measured; |
| 527 | 529 | } |
| 528 | 530 | |
| 529 | 531 | /** |
| 530 | - * Build the `:root` declaration block. | |
| 532 | + * The radii the store owner set by hand, role => length. | |
| 531 | 533 | * |
| 534 | + * Stored as typed lengths (a bare number is pixels); re-checked here the | |
| 535 | + * same way the sanitiser checks them on save, so a stale or hand-edited | |
| 536 | + * option never reaches the page. | |
| 537 | + * | |
| 538 | + * @return array Role key => length (`12px`, `0.5rem`, `1em`). | |
| 539 | + */ | |
| 540 | + public static function getCustomRadii(): array | |
| 541 | + { | |
| 542 | + $stored = (new StoreSettings())->get('appearance_radius', []); | |
| 543 | + | |
| 544 | + return RadiusPalette::sanitizeOwnerMap($stored); | |
| 545 | + } | |
| 546 | + | |
| 547 | + /** | |
| 548 | + * The radii the active theme states, role => length. | |
| 549 | + * | |
| 550 | + * @return array | |
| 551 | + */ | |
| 552 | + protected static function getThemeRadii(): array | |
| 553 | + { | |
| 554 | + $radii = ThemePalette::radii(); | |
| 555 | + | |
| 556 | + return is_array($radii) ? $radii : []; | |
| 557 | + } | |
| 558 | + | |
| 559 | + /** | |
| 560 | + * Every radius that will actually be written, role => normalised length. | |
| 561 | + * | |
| 562 | + * Same three sources as the colours: the owner's own radii under | |
| 563 | + * `customize`, the theme's under `inherit_from_theme`, and none under | |
| 564 | + * `default` — FluentCart's look must not move. | |
| 565 | + * | |
| 566 | + * @return array | |
| 567 | + */ | |
| 568 | + public static function getEffectiveRadii(): array | |
| 569 | + { | |
| 570 | + return self::radiiFor(self::getSource()); | |
| 571 | + } | |
| 572 | + | |
| 573 | + /** | |
| 574 | + * The radii a given source would write, after the storefront filter. | |
| 575 | + * | |
| 576 | + * The settings preview asks for the inherited radii while another source | |
| 577 | + * is saved, so it shows exactly what choosing that source would print. | |
| 578 | + * | |
| 579 | + * @param string $source One of the ColorPalette::SOURCE_* constants. | |
| 580 | + * @return array Role key => normalised length. | |
| 581 | + */ | |
| 582 | + public static function radiiFor(string $source): array | |
| 583 | + { | |
| 584 | + if ($source === ColorPalette::SOURCE_CUSTOM) { | |
| 585 | + $radii = self::getCustomRadii(); | |
| 586 | + } elseif ($source === ColorPalette::SOURCE_THEME) { | |
| 587 | + $radii = self::getThemeRadii(); | |
| 588 | + } else { | |
| 589 | + $radii = []; | |
| 590 | + } | |
| 591 | + | |
| 592 | + /** | |
| 593 | + * Filter the storefront border radii before they are written to the page. | |
| 594 | + * | |
| 595 | + * @param array $radii Role key (card|btn|input) => length (`0`, `8px`, `0.5rem`). | |
| 596 | + * @param array $context Read-only context: ['source' => appearance source]. | |
| 597 | + */ | |
| 598 | + $filtered = apply_filters('fluent_cart/theme/storefront_radii', $radii, [ | |
| 599 | + 'source' => $source, | |
| 600 | + ]); | |
| 601 | + | |
| 602 | + if (!is_array($filtered)) { | |
| 603 | + $filtered = $radii; | |
| 604 | + } | |
| 605 | + | |
| 606 | + // Re-checked after the filter: registry roles only, one plain length | |
| 607 | + // each, in registry order. | |
| 608 | + $effective = []; | |
| 609 | + | |
| 610 | + foreach (array_keys(RadiusPalette::roles()) as $role) { | |
| 611 | + if (!isset($filtered[$role])) { | |
| 612 | + continue; | |
| 613 | + } | |
| 614 | + | |
| 615 | + $length = RadiusPalette::sanitizeLength($filtered[$role]); | |
| 616 | + | |
| 617 | + if ($length !== '') { | |
| 618 | + $effective[$role] = $length; | |
| 619 | + } | |
| 620 | + } | |
| 621 | + | |
| 622 | + return $effective; | |
| 623 | + } | |
| 624 | + | |
| 625 | + /** | |
| 626 | + * Build the `:root` declaration block: colours first, then radii. | |
| 627 | + * | |
| 628 | + * Either half prints without the other — a store can set radii and keep | |
| 629 | + * FluentCart's colours, or the reverse. | |
| 630 | + * | |
| 532 | 631 | * @return string CSS, or '' when there is nothing to write. |
| 533 | 632 | */ |
| 534 | 633 | public static function buildCss(): string |
| 535 | 634 | { |
| 635 | + $declarations = self::buildColorDeclarations() . self::buildRadiusDeclarations(); | |
| 636 | + | |
| 637 | + return $declarations === '' ? '' : ':root{' . $declarations . '}'; | |
| 638 | + } | |
| 639 | + | |
| 640 | + /** | |
| 641 | + * The radius declarations, without the surrounding block. | |
| 642 | + * | |
| 643 | + * @return string | |
| 644 | + */ | |
| 645 | + protected static function buildRadiusDeclarations(): string | |
| 646 | + { | |
| 647 | + $roles = RadiusPalette::roles(); | |
| 648 | + $declarations = ''; | |
| 649 | + | |
| 650 | + foreach (self::getEffectiveRadii() as $role => $length) { | |
| 651 | + if (!isset($roles[$role])) { | |
| 652 | + continue; | |
| 653 | + } | |
| 654 | + | |
| 655 | + $safe = RadiusPalette::sanitizeLength($length); | |
| 656 | + | |
| 657 | + if ($safe !== '') { | |
| 658 | + $declarations .= $roles[$role]['var'] . ':' . $safe . ';'; | |
| 659 | + } | |
| 660 | + } | |
| 661 | + | |
| 662 | + return $declarations; | |
| 663 | + } | |
| 664 | + | |
| 665 | + /** | |
| 666 | + * The colour declarations, without the surrounding block. | |
| 667 | + * | |
| 668 | + * @return string | |
| 669 | + */ | |
| 670 | + protected static function buildColorDeclarations(): string | |
| 671 | + { | |
| 536 | 672 | $colors = self::getEffectiveColors(); |
| 537 | 673 | |
| 538 | 674 | if (!$colors) { |
| 539 | 675 | return ''; |
| @@ -559,7 +695,7 @@ | ||
| 559 | 695 | $declarations .= $property . ':' . $safe . ';'; |
| 560 | 696 | } |
| 561 | 697 | } |
| 562 | 698 | |
| 563 | - return $declarations === '' ? '' : ':root{' . $declarations . '}'; | |
| 699 | + return $declarations; | |
| 564 | 700 | } |
| 565 | 701 | } |