PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
← All changes | app/Services/Theme/ThemePalette.php +24 -338 trunk → 1.6.5 View file →
@@ -1,14 +1,8 @@
1 1 <?php
2 2
3 3 namespace FluentCart\App\Services\Theme;
4 4
5 -use FluentCart\App\Services\Theme\Readers\AstraSettingsReader;
6 -use FluentCart\App\Services\Theme\Readers\BlocksySettingsReader;
7 -use FluentCart\App\Services\Theme\Readers\BricksSettingsReader;
8 -use FluentCart\App\Services\Theme\Readers\DiviSettingsReader;
9 -use FluentCart\App\Services\Theme\Readers\GeneratePressSettingsReader;
10 -use FluentCart\App\Services\Theme\Readers\KadenceSettingsReader;
11 5 use FluentCart\Framework\Support\Arr;
12 6
13 7 /**
14 8 * Reads the active theme's colour palette and resolves it into the semantic
@@ -28,10 +22,9 @@
28 22 *
29 23 * These are the slugs themes agree on — `base`/`contrast` come from Twenty
30 24 * Twenty-Four and the themes that copied it, `primary`/`accent` from the
31 25 * classic-adjacent ones, and GeneratePress publishes the same names as
32 - * references its own stylesheets resolve (measured through its Global
33 - * Colors; see vendorValues()).
26 + * references its own stylesheets resolve.
34 27 *
35 28 * Three vendors are supported by name — the ones popular enough to be worth
36 29 * carrying, each mapping written down from the theme's own sources rather
37 30 * than inferred from the numbering:
@@ -88,50 +81,8 @@
88 81 */
89 82 protected static $cachedVendorValues = null;
90 83
91 84 /**
92 - * Request-level cache for the roles the theme's own settings state.
93 - *
94 - * @var array|null
95 - */
96 - protected static $cachedSettingsRoles = null;
97 -
98 - /**
99 - * Theme settings readers, keyed by the theme name passed to the
100 - * `fluent_cart/theme/settings_roles` filter. The first that applies wins.
101 - *
102 - * Only one theme runs on a site, so the order only matters when several
103 - * vendors' functions are loaded at once (the test stubs). The stricter
104 - * checks go first: Blocksy's and Kadence's each need a live instance from
105 - * the vendor's API, Divi's needs the `$shortname` global to name Divi, and
106 - * Bricks' needs its running `Bricks\Theme` singleton, and GeneratePress's
107 - * needs its dynamic-CSS printer hooked on `wp_enqueue_scripts`, none of
108 - * which a loaded-but-idle stub provides, while Astra's is a bare
109 - * `function_exists()` that stays true once its stub is loaded — so Astra
110 - * is asked last. Among the strict ones the order is arbitrary.
111 - *
112 - * @var array
113 - */
114 - protected static $settingsReaders = [
115 - 'blocksy' => BlocksySettingsReader::class,
116 - 'kadence' => KadenceSettingsReader::class,
117 - 'divi' => DiviSettingsReader::class,
118 - 'bricks' => BricksSettingsReader::class,
119 - 'generatepress' => GeneratePressSettingsReader::class,
120 - 'astra' => AstraSettingsReader::class,
121 - ];
122 -
123 - /**
124 - * The roles a theme settings reader may state.
125 - *
126 - * @var array
127 - */
128 - protected static $settingsRoleKeys = [
129 - 'surface', 'text', 'accent', 'border',
130 - 'button_bg', 'button_text', 'button_hover_bg', 'button_hover_text',
131 - ];
132 -
133 - /**
134 85 * Drop the request-level caches.
135 86 *
136 87 * Reading theme.json and resolving eleven roles is repeated work within a
137 88 * request, so both are cached. Anything that changes what those reads would
@@ -144,9 +95,8 @@
144 95 {
145 96 self::$cachedPalette = null;
146 97 self::$cachedRoles = null;
147 98 self::$cachedVendorValues = null;
148 - self::$cachedSettingsRoles = null;
149 99 }
150 100
151 101 /**
152 102 * The active theme's colour palette, normalised to hex.
@@ -311,19 +261,10 @@
311 261 * option being read. Astra prints `--ast-global-color-N` from
312 262 * `astra_get_option('global-color-palette')` (its
313 263 * `generate_global_palette_style()`), and Kadence prints
314 264 * `--global-paletteN` from `kadence()->palette_option('paletteN')` (its
315 - * styles component; see KadenceSettingsReader::paletteValues()).
316 - * Blocksy's editor palette already ships with hex fallbacks, but its Customizer settings point at the bare
317 - * `--theme-palette-color-N`, so its palette is read too, from
318 - * `blocksy_manager()->colors->get_color_palette()` (the list it prints
319 - * the properties from; see BlocksySettingsReader::paletteValues()).
320 - * Bricks prints `--bricks-color-{id}` from its colour palette (or the
321 - * property a palette colour's raw value names; see
322 - * BricksSettingsReader::paletteValues()). GeneratePress prints `--{slug}`
323 - * on :root from its Global Colors and publishes them to the editor
324 - * palette as bare `var(--{slug})` (see
325 - * GeneratePressSettingsReader::paletteValues()).
265 + * styles component). Blocksy needs no entry: its palette already ships
266 + * with hex fallbacks.
326 267 *
327 268 * The values are only ever used as the fallback half of `var(--x, #hex)`,
328 269 * so a wrong or stale answer cannot repaint anything — the browser keeps
329 270 * resolving the live property — it can only mis-measure, which is where
@@ -347,24 +288,19 @@
347 288 $values['--ast-global-color-' . $index] = (string)$color;
348 289 }
349 290 }
350 291
351 - foreach (BlocksySettingsReader::paletteValues() as $property => $color) {
352 - $values[$property] = $color;
292 + if (function_exists('Kadence\\kadence')) {
293 + try {
294 + foreach (range(1, 15) as $index) {
295 + $values['--global-palette' . $index] = (string)\Kadence\kadence()->palette_option('palette' . $index);
296 + }
297 + } catch (\Throwable $e) {
298 + // The vendor's API misbehaving means no vendor values — the
299 + // stand-down paths below already handle that.
300 + }
353 301 }
354 302
355 - foreach (KadenceSettingsReader::paletteValues() as $property => $color) {
356 - $values[$property] = $color;
357 - }
358 -
359 - foreach (BricksSettingsReader::paletteValues() as $property => $color) {
360 - $values[$property] = $color;
361 - }
362 -
363 - foreach (GeneratePressSettingsReader::paletteValues() as $property => $color) {
364 - $values[$property] = $color;
365 - }
366 -
367 303 /**
368 304 * Filter the vendor-declared custom property values used to measure
369 305 * bare palette references, keyed by property name (`--x` => `#hex`).
370 306 *
@@ -455,115 +391,8 @@
455 391 return '';
456 392 }
457 393
458 394 /**
459 - * Normalise a colour a theme setting states into a value we can write.
460 - *
461 - * A hex is itself, lowercased. A custom-property reference stays a live
462 - * reference; a bare one gains the vendor's current value as its hex
463 - * fallback (see vendorValues()), so the browser still follows the
464 - * property while PHP measures the fallback. Anything else — rgba(),
465 - * named colours, expressions — is refused: the result is written into a
466 - * declaration on every storefront page and must pass
467 - * FrontendTheme::sanitizeDeclarationValue().
468 - *
469 - * @param mixed $raw
470 - * @return string Hex, `var(--x)`, `var(--x, #hex)`, or ''.
471 - */
472 - public static function settingValue($raw): string
473 - {
474 - if (!is_string($raw)) {
475 - return '';
476 - }
477 -
478 - $raw = trim($raw);
479 -
480 - if (preg_match('/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $raw)) {
481 - return ColorMath::hex($raw);
482 - }
483 -
484 - $reference = self::safeReference($raw);
485 -
486 - if ($reference === '' || self::measurable($reference) !== '') {
487 - return $reference;
488 - }
489 -
490 - $property = substr($reference, 4, -1);
491 - $vendorHex = (string)Arr::get(self::vendorValues(), $property, '');
492 -
493 - return $vendorHex !== '' ? 'var(' . $property . ', ' . $vendorHex . ')' : $reference;
494 - }
495 -
496 - /**
497 - * The colours the active theme's own settings state, by role.
498 - *
499 - * Palette slots say which colours a theme offers; its settings say which
500 - * one the owner put where. A reader for the active theme (see
501 - * Readers\ThemeSettingsReader) reports what the owner set — Astra's,
502 - * Blocksy's, Kadence's, Divi's, Bricks' or GeneratePress's Accent, Body Text, Content Background, Borders
503 - * and Button colours — and those outrank the palette-slot guesses in anchors() and resolve().
504 - * Only the site editor's own button (buttonGlobals()) outranks them.
505 - *
506 - * Roles: surface, text, accent, border, button_bg, button_text,
507 - * button_hover_bg, button_hover_text. A role the theme does not state is
508 - * absent, and is derived exactly as it would be without a reader.
509 - *
510 - * @return array Role => hex or reference.
511 - */
512 - public static function settingsRoles(): array
513 - {
514 - if (self::$cachedSettingsRoles !== null) {
515 - return self::$cachedSettingsRoles;
516 - }
517 -
518 - $roles = [];
519 - $theme = '';
520 -
521 - foreach (self::$settingsReaders as $name => $reader) {
522 - if ($reader::applies()) {
523 - $theme = $name;
524 - $roles = $reader::roles();
525 - break;
526 - }
527 - }
528 -
529 - /**
530 - * Filter the colours the active theme's settings state, by role.
531 - *
532 - * Runs whether or not a built-in reader applied, so a theme FluentCart
533 - * does not read can be supplied here; return [] to turn the reader off
534 - * and fall back to palette slots. Values must be a hex or a
535 - * `var(--x)` / `var(--x, #hex)` reference — anything else is dropped,
536 - * and a bare reference to a vendor property gains its hex fallback.
537 - *
538 - * @param array $roles Role => colour. Keys: surface, text, accent,
539 - * border, button_bg, button_text,
540 - * button_hover_bg, button_hover_text.
541 - * @param array $context ['theme' => reader name ('blocksy', 'kadence', 'divi', 'bricks', 'generatepress', 'astra'), or '' when
542 - * no built-in reader applies].
543 - */
544 - $roles = apply_filters('fluent_cart/theme/settings_roles', $roles, [
545 - 'theme' => $theme,
546 - ]);
547 -
548 - // A filter is just another source of values: only known roles with a
549 - // writable colour survive.
550 - $clean = [];
551 -
552 - if (is_array($roles)) {
553 - foreach (self::$settingsRoleKeys as $role) {
554 - $value = self::settingValue(Arr::get($roles, $role, ''));
555 -
556 - if ($value !== '') {
557 - $clean[$role] = $value;
558 - }
559 - }
560 - }
561 -
562 - return self::$cachedSettingsRoles = $clean;
563 - }
564 -
565 - /**
566 395 * Whether the active theme gave us anything usable.
567 396 *
568 397 * @return bool
569 398 */
@@ -598,12 +427,8 @@
598 427 if (Arr::get($globals, 'background', '') !== '' || Arr::get($globals, 'text', '') !== '') {
599 428 return true;
600 429 }
601 430
602 - if (self::settingsRoles()) {
603 - return true;
604 - }
605 -
606 431 return Arr::get(self::buttonGlobals(), 'background', '') !== '';
607 432 }
608 433
609 434 /**
@@ -693,17 +518,13 @@
693 518 * Backgrounds usually arrive as a preset reference
694 519 * (`var:preset|color|contrast`), which resolves through the palette the
695 520 * same way the page pair's do.
696 521 *
697 - * The `:hover` pair (`styles.elements.button.:hover.color`) is read the
698 - * same way, into `hover_background` / `hover_text`.
699 - *
700 - * @return array ['background' => hex, 'text' => hex, 'hover_background' => hex,
701 - * 'hover_text' => hex]; any may be ''.
522 + * @return array ['background' => hex, 'text' => hex]; either may be ''.
702 523 */
703 524 public static function buttonGlobals(): array
704 525 {
705 - $colors = ['background' => '', 'text' => '', 'hover_background' => '', 'hover_text' => ''];
526 + $colors = ['background' => '', 'text' => ''];
706 527
707 528 if (!class_exists('WP_Theme_JSON_Resolver')) {
708 529 return $colors;
709 530 }
@@ -718,21 +539,15 @@
718 539 if (!is_object($data) || !method_exists($data, 'get_raw_data')) {
719 540 continue;
720 541 }
721 542
722 - $raw = (array)$data->get_raw_data();
723 - $pairs = [
724 - '' => Arr::get($raw, 'styles.elements.button.color', []),
725 - 'hover_' => Arr::get($raw, 'styles.elements.button.:hover.color', []),
726 - ];
543 + $pair = Arr::get((array)$data->get_raw_data(), 'styles.elements.button.color', []);
727 544
728 - foreach ($pairs as $prefix => $pair) {
729 - foreach (['background', 'text'] as $half) {
730 - $value = self::resolveReference((string)Arr::get((array)$pair, $half, ''));
545 + foreach (['background', 'text'] as $half) {
546 + $value = self::resolveReference((string)Arr::get((array)$pair, $half, ''));
731 547
732 - if ($value !== '') {
733 - $colors[$prefix . $half] = $value;
734 - }
548 + if ($value !== '') {
549 + $colors[$half] = $value;
735 550 }
736 551 }
737 552 }
738 553
@@ -812,9 +627,8 @@
812 627 public static function anchors(): array
813 628 {
814 629 $map = self::anchorMap();
815 630 $globals = self::globalColors();
816 - $settings = self::settingsRoles();
817 631
818 632 // Surface and body text resolve as a PAIR from one source, never mixed
819 633 // from two. Global styles outrank the palette — the palette lists the
820 634 // AVAILABLE colours, global styles say what the body actually RENDERS,
@@ -826,33 +640,12 @@
826 640 // stylesheets' paired fallbacks never fire because both values exist.
827 641 $gsSurface = (string)Arr::get($globals, 'background', '');
828 642 $gsText = (string)Arr::get($globals, 'text', '');
829 643
830 - $setSurface = (string)Arr::get($settings, 'surface', '');
831 -
832 644 if ($gsSurface !== '' && $gsText !== '') {
833 645 // The rendered pair.
834 646 $surface = $gsSurface;
835 647 $text = $gsText;
836 - } elseif ($setSurface !== '') {
837 - // The pair the theme's settings state (Astra's Content Background
838 - // and Body Text). The same pair law: a lone stated text is never
839 - // mixed onto a guessed surface. A lone surface takes the palette's
840 - // own text when it reads there (4.5:1) — Astra saves Body Text
841 - // empty and still means its text swatch — and only an unreadable
842 - // one is replaced by a measured partner.
843 - $surface = $setSurface;
844 - $text = (string)Arr::get($settings, 'text', '');
845 - $surfaceHex = self::measurable($surface);
846 -
847 - if ($text === '' && $surfaceHex !== '') {
848 - $paletteText = self::value(Arr::get($map, 'text', ''));
849 - $paletteTextHex = self::measurable($paletteText);
850 -
851 - $text = $paletteTextHex !== '' && ColorMath::contrast($surfaceHex, $paletteTextHex) >= 4.5
852 - ? $paletteText
853 - : ColorMath::readableOn($surfaceHex, '#F3F4F6', '#2F3448');
854 - }
855 648 } else {
856 649 // The published pair. A lone global-styles fragment is dropped
857 650 // rather than paired with a guess.
858 651 $surface = self::value(Arr::get($map, 'surface', ''));
@@ -866,14 +659,10 @@
866 659 $text = ColorMath::readableOn($surface, '#F3F4F6', '#2F3448');
867 660 }
868 661 }
869 662
870 - $accent = (string)Arr::get($settings, 'accent', '');
663 + $accent = self::value(Arr::get($map, 'accent', ''));
871 664
872 - if ($accent === '') {
873 - $accent = self::value(Arr::get($map, 'accent', ''));
874 - }
875 -
876 665 // The button follows the same law as the page pair: what the owner
877 666 // set in the site editor (Styles → Buttons) outranks every guess the
878 667 // palette could offer — but only led by its background. A background
879 668 // brings its own text partner, or gets one measured against it in
@@ -884,12 +673,8 @@
884 673
885 674 if ($button['background'] !== '') {
886 675 $buttonBg = $button['background'];
887 676 $buttonText = $button['text'];
888 - } elseif (Arr::get($settings, 'button_bg', '') !== '') {
889 - // Next, the button the theme's settings state, with its partner.
890 - $buttonBg = (string)$settings['button_bg'];
891 - $buttonText = (string)Arr::get($settings, 'button_text', '');
892 677 } else {
893 678 $buttonBg = self::value(Arr::get($map, 'button_bg', ''));
894 679
895 680 if ($buttonBg === '') {
@@ -949,12 +734,9 @@
949 734 'surface_mute' => ColorMath::mix($textHex, $surfaceHex, 8),
950 735 'divider' => ColorMath::mix($textHex, $surfaceHex, 10),
951 736 'border' => ColorMath::mix($textHex, $surfaceHex, 18),
952 737 'text_placeholder' => ColorMath::mix($textHex, $surfaceHex, 45),
953 - // Muted text is still text: it moves toward the body text only
954 - // as far as it must to read (4.5:1). A mid-grey body text
955 - // (Divi's #666666) otherwise mixed down to 2.92:1.
956 - 'text_muted' => ColorMath::readableMix($textHex, $surfaceHex, 68),
738 + 'text_muted' => ColorMath::mix($textHex, $surfaceHex, 68),
957 739 ]
958 740 : [
959 741 'surface_alt' => '',
960 742 'surface_mute' => '',
@@ -963,16 +745,8 @@
963 745 'text_placeholder' => '',
964 746 'text_muted' => '',
965 747 ];
966 748
967 - // A border the theme's settings state is the border (Astra's Borders);
968 - // only an unstated one is mixed.
969 - $settings = self::settingsRoles();
970 -
971 - if (Arr::get($settings, 'border', '') !== '') {
972 - $derived['border'] = (string)$settings['border'];
973 - }
974 -
975 749 // Contrast needs a real colour to measure against for the same reason.
976 750 // And the button is owned as a pair or not at all: a background whose
977 751 // value cannot be measured (a bare reference — the theme resolves it on
978 752 // the page, and the store owner can recolour it to anything) is one no
@@ -978,62 +752,19 @@
978 752 // the page, and the store owner can recolour it to anything) is one no
979 753 // readable text can be paired with here, so neither half is written and
980 754 // the stylesheets' own paired fallback styles the button instead. Text
981 755 // the owner chose alongside the background (the site editor's button
982 - // pair, or a theme settings reader's) is worn as given unless it cannot
983 - // be read on it (below 3:1); a missing or unreadable partner is measured.
756 + // pair) is worn as given; only a missing partner is measured.
984 757 $buttonBgHex = self::measurable($buttonBg);
985 - $ownText = self::statedTextOn((string)Arr::get($anchors, 'button_text', ''), $buttonBgHex);
758 + $ownText = (string)Arr::get($anchors, 'button_text', '');
986 759
987 760 if ($buttonBgHex !== '') {
988 - $derived['button_text'] = $ownText !== '' ? $ownText : ColorMath::readableText($buttonBgHex);
761 + $derived['button_text'] = $ownText !== '' ? $ownText : ColorMath::readableOn($buttonBgHex);
989 762 } else {
990 763 $anchors['button_bg'] = '';
991 764 $derived['button_text'] = '';
992 765 }
993 766
994 - // The hover follows the button it belongs to: unwritten when the button
995 - // is. The site editor's `:hover` pair outranks any guess; otherwise the
996 - // button colour moves away from itself — darker for a light button,
997 - // lighter for a dark one. Hover text given with it is worn as given;
998 - // otherwise the resting text carries over while it still reads (WCAG
999 - // 4.5:1), and a partner is measured when it does not.
1000 - $derived['button_hover_bg'] = '';
1001 - $derived['button_hover_text'] = '';
1002 -
1003 - if ($buttonBgHex !== '') {
1004 - $stated = self::buttonGlobals();
1005 - $hoverBg = $stated['hover_background'];
1006 - $hoverText = $stated['hover_text'];
1007 -
1008 - // The theme settings' hover belongs to the theme settings' button:
1009 - // it is only used when that button is the one being worn, never
1010 - // paired onto a button the site editor states.
1011 - $settingsButton = $stated['background'] === '' && Arr::get($settings, 'button_bg', '') !== '';
1012 -
1013 - if ($hoverBg === '' && $settingsButton && Arr::get($settings, 'button_hover_bg', '') !== '') {
1014 - $hoverBg = (string)$settings['button_hover_bg'];
1015 -
1016 - if ($hoverText === '') {
1017 - $hoverText = (string)Arr::get($settings, 'button_hover_text', '');
1018 - }
1019 - }
1020 -
1021 - if ($hoverBg === '') {
1022 - $hoverBg = ColorMath::shiftFromItself($buttonBgHex, 12);
1023 - }
1024 -
1025 - $hoverBgHex = self::measurable($hoverBg);
1026 - $hoverText = self::statedTextOn($hoverText, $hoverBgHex);
1027 -
1028 - if ($hoverText === '' && $hoverBgHex !== '') {
1029 - $hoverText = self::hoverTextFor($hoverBgHex, (string)$derived['button_text']);
1030 - }
1031 -
1032 - $derived['button_hover_bg'] = $hoverBg;
1033 - $derived['button_hover_text'] = $hoverText;
1034 - }
1035 -
1036 767 // The outline secondary button is FluentCart's own invention, and its
1037 768 // outline is the hierarchy: it always keeps the page surface as its
1038 769 // background. Wearing the theme's stated pair outright painted BOTH
1039 770 // CTAs identically (Twenty Twenty-Five states black-on-white, so Add
@@ -1068,53 +799,8 @@
1068 799 */
1069 800 self::$cachedRoles = apply_filters('fluent_cart/theme/roles', array_merge($anchors, $derived));
1070 801
1071 802 return self::$cachedRoles;
1072 - }
1073 -
1074 - /**
1075 - * A text colour a theme or owner stated for a background, if it can be worn.
1076 - *
1077 - * Stated text is a choice and is worn as given — including brand pairs
1078 - * just under AA (white on #ff5500 is 3.21:1). Only one that cannot be read
1079 - * on its background (below 3:1, the large-text floor) is refused, so a
1080 - * measured partner takes its place. Twenty Twenty-Five's site-editor pair
1081 - * #111111 on #503aa8 (2.26:1) is the case this catches. A text or a
1082 - * background that cannot be measured here is not second-guessed.
1083 - *
1084 - * @param string $text The stated text, or ''.
1085 - * @param string $backgroundHex The measured background, or ''.
1086 - * @return string The text, or '' when it must be replaced.
1087 - */
1088 - public static function statedTextOn(string $text, string $backgroundHex): string
1089 - {
1090 - if ($text === '' || $backgroundHex === '') {
1091 - return $text;
1092 - }
1093 -
1094 - $textHex = self::measurable($text);
1095 -
1096 - if ($textHex === '') {
1097 - return $text;
1098 - }
1099 -
1100 - return ColorMath::contrast($backgroundHex, $textHex) >= 3 ? $text : '';
1101 - }
1102 -
1103 - /**
1104 - * The text for a hover background nobody gave a text: the button's
1105 - * resting text carries over while it still reads (WCAG 4.5:1), and a
1106 - * partner is measured when it does not.
1107 - *
1108 - * @param string $hoverBgHex
1109 - * @param string $restingText
1110 - * @return string
1111 - */
1112 - public static function hoverTextFor(string $hoverBgHex, string $restingText): string
1113 - {
1114 - return ColorMath::contrast($hoverBgHex, $restingText) >= 4.5
1115 - ? $restingText
1116 - : ColorMath::readableText($hoverBgHex);
1117 803 }
1118 804
1119 805 /**
1120 806 * A one-line description of what the active theme offers, shown under the