PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 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 All 51 releases
← All changes | app/Services/Theme/ThemePalette.php +391 -72 1.6.6 → 1.7.1 View file →
@@ -7,8 +7,9 @@
7 7 use FluentCart\App\Services\Theme\Readers\BricksSettingsReader;
8 8 use FluentCart\App\Services\Theme\Readers\DiviSettingsReader;
9 9 use FluentCart\App\Services\Theme\Readers\GeneratePressSettingsReader;
10 10 use FluentCart\App\Services\Theme\Readers\KadenceSettingsReader;
11 +use FluentCart\App\Services\Theme\Readers\ThemeRadiusReader;
11 12 use FluentCart\Framework\Support\Arr;
12 13
13 14 /**
14 15 * Reads the active theme's colour palette and resolves it into the semantic
@@ -95,8 +96,22 @@
95 96 */
96 97 protected static $cachedSettingsRoles = null;
97 98
98 99 /**
100 + * @var array|null
101 + */
102 + protected static $cachedRadii = null;
103 +
104 + /**
105 + * Whether the site switched the theme's settings reader off: a reader
106 + * stated colours and `fluent_cart/theme/settings_roles` returned none.
107 + * Set by settingsRoles().
108 + *
109 + * @var bool
110 + */
111 + protected static $settingsReaderOff = false;
112 +
113 + /**
99 114 * Theme settings readers, keyed by the theme name passed to the
100 115 * `fluent_cart/theme/settings_roles` filter. The first that applies wins.
101 116 *
102 117 * Only one theme runs on a site, so the order only matters when several
@@ -145,8 +160,10 @@
145 160 self::$cachedPalette = null;
146 161 self::$cachedRoles = null;
147 162 self::$cachedVendorValues = null;
148 163 self::$cachedSettingsRoles = null;
164 + self::$cachedRadii = null;
165 + self::$settingsReaderOff = false;
149 166 }
150 167
151 168 /**
152 169 * The active theme's colour palette, normalised to hex.
@@ -525,8 +542,10 @@
525 542 break;
526 543 }
527 544 }
528 545
546 + $stated = $roles;
547 +
529 548 /**
530 549 * Filter the colours the active theme's settings state, by role.
531 550 *
532 551 * Runs whether or not a built-in reader applied, so a theme FluentCart
@@ -558,12 +577,331 @@
558 577 }
559 578 }
560 579 }
561 580
581 + // Returning [] for a reader that stated something is the documented
582 + // way to switch it off; radii() honours that for the same reader.
583 + self::$settingsReaderOff = !empty($stated) && is_array($roles) && $roles === [];
584 +
562 585 return self::$cachedSettingsRoles = $clean;
563 586 }
564 587
565 588 /**
589 + * The border radii the active theme states, by role.
590 + *
591 + * theme.json speaks first, as it does for the button colours
592 + * (buttonGlobals()): the button element, else the Button block, and the
593 + * text-input element, else select, else the Search block — the owner's
594 + * site editor and picked style variation over the theme's own, see
595 + * radiusGlobals(). Then the first applying settings reader that
596 + * implements Readers\ThemeRadiusReader fills the roles the site editor
597 + * left unstated — unless the site switched that reader off by returning
598 + * [] from `fluent_cart/theme/settings_roles`, which turns off its radii
599 + * too. A role nothing states is absent, and FluentCart prints nothing
600 + * for it.
601 + *
602 + * Roles: card, btn, input (RadiusPalette::roles()). Every value is one
603 + * length in RadiusPalette::sanitizeLength()'s grammar; anything else is
604 + * dropped rather than guessed.
605 + *
606 + * @return array Role => length (`0`, `8px`, `0.5rem`, `1em`).
607 + */
608 + public static function radii(): array
609 + {
610 + if (self::$cachedRadii !== null) {
611 + return self::$cachedRadii;
612 + }
613 +
614 + $radii = self::radiusGlobals();
615 + $readerName = '';
616 +
617 + // A reader the site switched off through `fluent_cart/theme/settings_roles`
618 + // states no radius either. The site editor is not a reader and still counts.
619 + self::settingsRoles();
620 + $readers = self::$settingsReaderOff ? [] : self::$settingsReaders;
621 +
622 + foreach ($readers as $name => $reader) {
623 + if (!$reader::applies() || !is_subclass_of($reader, ThemeRadiusReader::class)) {
624 + continue;
625 + }
626 +
627 + $readerName = $name;
628 +
629 + foreach ((array)$reader::radii() as $role => $value) {
630 + if (!isset($radii[$role])) {
631 + $radii[$role] = $value;
632 + }
633 + }
634 +
635 + break;
636 + }
637 +
638 + /**
639 + * Filter the border radii the active theme states, by role.
640 + *
641 + * Runs whether or not a built-in reader applied, so a theme FluentCart
642 + * does not read can be supplied here; return [] to state none. Every
643 + * value is normalised again: only `card`, `btn` and `input` survive,
644 + * each as one length (`0`, or a number in px, rem or em).
645 + *
646 + * @param array $radii Role => length. Keys: card, btn, input.
647 + * @param array $context ['theme' => the active template's slug,
648 + * 'reader' => the reader that applied ('blocksy', 'kadence',
649 + * 'divi', 'bricks', 'astra'), or '' when none did].
650 + */
651 + $radii = apply_filters('fluent_cart/theme/radius_roles', $radii, [
652 + 'theme' => get_template(),
653 + 'reader' => $readerName,
654 + ]);
655 +
656 + $clean = [];
657 +
658 + if (is_array($radii)) {
659 + foreach (array_keys(RadiusPalette::roles()) as $role) {
660 + $value = RadiusPalette::sanitizeLength(Arr::get($radii, $role, ''));
661 +
662 + if ($value !== '') {
663 + $clean[$role] = $value;
664 + }
665 + }
666 + }
667 +
668 + return self::$cachedRadii = $clean;
669 + }
670 +
671 + /**
672 + * The radii theme.json states, from the `theme` and `user` origins — the
673 + * owner's (the site editor, or a style variation they picked, both saved
674 + * in the user origin) over the theme's. Core's origin is left out, as in
675 + * buttonGlobals().
676 + *
677 + * Within one origin each role reads a chain, the closest match first:
678 + * - btn: the button element (`styles.elements.button`, what a plain
679 + * `<button class="wp-element-button">` wears — FluentCart's
680 + * buttons are plain buttons), then the Button block
681 + * (`styles.blocks.core/button`);
682 + * - input: the text-input element, the select element, then the Search
683 + * block (`styles.blocks.core/search`, whose border styles its
684 + * input).
685 + * No path is read as the product-card radius: no theme.json path names a
686 + * product card, and a featured-image or group radius is not one.
687 + *
688 + * @return array Role => length, normalised; only stated roles.
689 + */
690 + protected static function radiusGlobals(): array
691 + {
692 + $radii = [];
693 +
694 + if (!class_exists('WP_Theme_JSON_Resolver')) {
695 + return $radii;
696 + }
697 +
698 + $paths = [
699 + 'btn' => [
700 + 'styles.elements.button.border.radius',
701 + 'styles.blocks.core/button.border.radius',
702 + ],
703 + 'input' => [
704 + 'styles.elements.textInput.border.radius',
705 + 'styles.elements.select.border.radius',
706 + 'styles.blocks.core/search.border.radius',
707 + ],
708 + ];
709 +
710 + foreach (['get_theme_data', 'get_user_data'] as $origin) {
711 + if (!method_exists('WP_Theme_JSON_Resolver', $origin)) {
712 + continue;
713 + }
714 +
715 + $data = \WP_Theme_JSON_Resolver::$origin();
716 +
717 + if (!is_object($data) || !method_exists($data, 'get_raw_data')) {
718 + continue;
719 + }
720 +
721 + $raw = (array)$data->get_raw_data();
722 +
723 + foreach ($paths as $role => $candidates) {
724 + foreach ($candidates as $path) {
725 + $stated = Arr::get($raw, $path);
726 +
727 + if ($stated === null || $stated === '' || $stated === []) {
728 + continue;
729 + }
730 +
731 + // The first stated path ends the chain, writable or not:
732 + // a later path only stands in when the earlier is unset.
733 + // An unwritable one leaves the role to the theme's reader.
734 + $value = self::radiusValue($stated);
735 +
736 + if ($value !== '') {
737 + $radii[$role] = $value;
738 + } else {
739 + unset($radii[$role]);
740 + }
741 +
742 + break;
743 + }
744 + }
745 + }
746 +
747 + return $radii;
748 + }
749 +
750 + /**
751 + * One theme.json radius as a single length.
752 + *
753 + * - `{"ref": "styles.…"}` points at another value in the merged
754 + * theme.json data and is followed (at most three hops);
755 + * - a four-corner object is one length only when every corner is;
756 + * - preset references resolve to the preset's size:
757 + * `var:preset|border-radius|slug` / `var(--wp--preset--border-radius--slug)`
758 + * through `settings.border.radiusSizes`, and
759 + * `var:preset|spacing|slug` / `var(--wp--preset--spacing--slug)`
760 + * through `settings.spacing.spacingSizes` (a fluid `min()`/`clamp()`
761 + * size fails the grammar and is dropped);
762 + * - `var(--wp--custom--…)` resolves to the custom value it names.
763 + *
764 + * @param mixed $value
765 + * @param int $hops Ref hops left.
766 + * @return string Normalised length, or ''.
767 + */
768 + protected static function radiusValue($value, int $hops = 3): string
769 + {
770 + if (is_array($value)) {
771 + if (isset($value['ref'])) {
772 + if ($hops <= 0 || !is_string($value['ref']) || $value['ref'] === '') {
773 + return '';
774 + }
775 +
776 + return self::radiusValue(Arr::get(self::mergedThemeJson(), $value['ref']), $hops - 1);
777 + }
778 +
779 + $corners = [];
780 +
781 + foreach (['topLeft', 'topRight', 'bottomRight', 'bottomLeft'] as $corner) {
782 + $corners[] = self::radiusValue(Arr::get($value, $corner, ''), $hops);
783 + }
784 +
785 + $corners = array_unique($corners);
786 +
787 + return count($corners) === 1 ? (string)reset($corners) : '';
788 + }
789 +
790 + if (!is_string($value)) {
791 + return RadiusPalette::sanitizeLength($value);
792 + }
793 +
794 + $value = trim($value);
795 +
796 + if (preg_match('/^var:preset\|(border-radius|spacing)\|([\w-]+)$/', $value, $matches)
797 + || preg_match('/^var\(\s*--wp--preset--(border-radius|spacing)--([\w-]+)\s*\)$/', $value, $matches)) {
798 + $setting = $matches[1] === 'spacing' ? ['spacing', 'spacingSizes'] : ['border', 'radiusSizes'];
799 +
800 + return RadiusPalette::sanitizeLength(self::presetSize($setting, $matches[2]));
801 + }
802 +
803 + if (preg_match('/^var\(\s*--wp--custom--([\w-]+)\s*\)$/', $value, $matches)) {
804 + return RadiusPalette::sanitizeLength(self::customSetting($matches[1]));
805 + }
806 +
807 + return RadiusPalette::sanitizeLength($value);
808 + }
809 +
810 + /**
811 + * The merged theme.json data (core, theme, user) a `ref` resolves
812 + * against — the same data WordPress resolves refs against when it prints.
813 + *
814 + * @return array
815 + */
816 + protected static function mergedThemeJson(): array
817 + {
818 + if (!class_exists('WP_Theme_JSON_Resolver') || !method_exists('WP_Theme_JSON_Resolver', 'get_merged_data')) {
819 + return [];
820 + }
821 +
822 + $data = \WP_Theme_JSON_Resolver::get_merged_data();
823 +
824 + return is_object($data) && method_exists($data, 'get_raw_data') ? (array)$data->get_raw_data() : [];
825 + }
826 +
827 + /**
828 + * The size of a preset (`settings.border.radiusSizes` or
829 + * `settings.spacing.spacingSizes`), the owner's over the theme's over
830 + * core's.
831 + *
832 + * @param array $setting Settings path, e.g. ['spacing', 'spacingSizes'].
833 + * @param string $slug
834 + * @return string
835 + */
836 + protected static function presetSize(array $setting, string $slug): string
837 + {
838 + if (!function_exists('wp_get_global_settings')) {
839 + return '';
840 + }
841 +
842 + $sizes = wp_get_global_settings($setting);
843 +
844 + if (!is_array($sizes)) {
845 + return '';
846 + }
847 +
848 + // Merged settings are keyed by origin; a single origin is a list.
849 + $lists = isset($sizes[0]) ? [$sizes] : array_values(array_intersect_key(
850 + $sizes,
851 + array_flip(['default', 'theme', 'custom'])
852 + ));
853 +
854 + $size = '';
855 +
856 + foreach ($lists as $list) {
857 + foreach ((array)$list as $preset) {
858 + if (is_array($preset) && (string)Arr::get($preset, 'slug') === $slug && is_string(Arr::get($preset, 'size'))) {
859 + $size = $preset['size'];
860 + }
861 + }
862 + }
863 +
864 + return $size;
865 + }
866 +
867 + /**
868 + * The `settings.custom` value a `--wp--custom--a--b` property is printed
869 + * from. WordPress names each level by its key in kebab case, joined by
870 + * `--`, so the property is walked one level at a time.
871 + *
872 + * @param string $property The part after `--wp--custom--`.
873 + * @return string
874 + */
875 + protected static function customSetting(string $property): string
876 + {
877 + if (!function_exists('wp_get_global_settings')) {
878 + return '';
879 + }
880 +
881 + $node = wp_get_global_settings(['custom']);
882 +
883 + foreach (explode('--', $property) as $segment) {
884 + if (!is_array($node)) {
885 + return '';
886 + }
887 +
888 + $next = null;
889 +
890 + foreach ($node as $key => $child) {
891 + if (_wp_to_kebab_case((string)$key) === $segment) {
892 + $next = $child;
893 + break;
894 + }
895 + }
896 +
897 + $node = $next;
898 + }
899 +
900 + return is_string($node) || is_int($node) || is_float($node) ? (string)$node : '';
901 + }
902 +
903 + /**
566 904 * Whether the active theme gave us anything usable.
567 905 *
568 906 * @return bool
569 907 */
@@ -577,10 +915,11 @@
577 915 *
578 916 * A theme can publish a palette we cannot read — several popular ones
579 917 * declare theirs as `var(--theme-colour-0)` references — and it can set a
580 918 * global background and text colour without publishing a palette. A site
581 - * can also state nothing but its button pair (Styles → Buttons in the
582 - * site editor), and that is still an explicit configuration to wear.
919 + * can also state nothing but its button colours (Styles → Buttons in the
920 + * site editor) — a background, a text, or both — and that is still an
921 + * explicit configuration to wear.
583 922 * Only when all of them come back empty is there genuinely nothing to
584 923 * inherit, and in that case inheriting must stay out of the way rather
585 924 * than write FluentCart's own colours back to the page and call them the
586 925 * theme's.
@@ -602,9 +941,11 @@
602 941 if (self::settingsRoles()) {
603 942 return true;
604 943 }
605 944
606 - return Arr::get(self::buttonGlobals(), 'background', '') !== '';
945 + $button = self::buttonGlobals();
946 +
947 + return Arr::get($button, 'background', '') !== '' || Arr::get($button, 'text', '') !== '';
607 948 }
608 949
609 950 /**
610 951 * Look up one palette colour by slug.
@@ -814,17 +1155,14 @@
814 1155 $map = self::anchorMap();
815 1156 $globals = self::globalColors();
816 1157 $settings = self::settingsRoles();
817 1158
818 - // Surface and body text resolve as a PAIR from one source, never mixed
819 - // from two. Global styles outrank the palette — the palette lists the
820 - // AVAILABLE colours, global styles say what the body actually RENDERS,
821 - // and on Twenty Twenty-Five's dark style variation those disagree —
822 - // but only when global styles supply BOTH halves. A site that sets
823 - // only its text (light, say, over a CSS-painted dark background) must
824 - // not have that fragment mixed onto the palette's guessed white
825 - // surface: that is light text printed onto a light panel, and the
826 - // stylesheets' paired fallbacks never fire because both values exist.
1159 + // Global styles outrank the palette — the palette lists the AVAILABLE
1160 + // colours, global styles say what the body actually RENDERS, and on
1161 + // Twenty Twenty-Five's dark style variation those disagree. The
1162 + // surface comes from global styles, then the theme's settings, then
1163 + // the palette. A body text the owner stated is worn as given below,
1164 + // whichever source the surface came from.
827 1165 $gsSurface = (string)Arr::get($globals, 'background', '');
828 1166 $gsText = (string)Arr::get($globals, 'text', '');
829 1167
830 1168 $setSurface = (string)Arr::get($settings, 'surface', '');
@@ -834,13 +1172,12 @@
834 1172 $surface = $gsSurface;
835 1173 $text = $gsText;
836 1174 } elseif ($setSurface !== '') {
837 1175 // 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.
1176 + // and Body Text). A surface stated without a text takes the
1177 + // palette's own text when it reads there (4.5:1) — Astra saves
1178 + // Body Text empty and still means its text swatch — and only an
1179 + // unreadable one is replaced by a measured partner.
843 1180 $surface = $setSurface;
844 1181 $text = (string)Arr::get($settings, 'text', '');
845 1182 $surfaceHex = self::measurable($surface);
846 1183
@@ -852,10 +1189,9 @@
852 1189 ? $paletteText
853 1190 : ColorMath::readableOn($surfaceHex, '#F3F4F6', '#2F3448');
854 1191 }
855 1192 } else {
856 - // The published pair. A lone global-styles fragment is dropped
857 - // rather than paired with a guess.
1193 + // The published pair.
858 1194 $surface = self::value(Arr::get($map, 'surface', ''));
859 1195 $text = self::value(Arr::get($map, 'text', ''));
860 1196
861 1197 if ($text === '' && ColorMath::hex($surface) !== '') {
@@ -866,8 +1202,16 @@
866 1202 $text = ColorMath::readableOn($surface, '#F3F4F6', '#2F3448');
867 1203 }
868 1204 }
869 1205
1206 + // A body text the owner chose is worn as given, even without a stated
1207 + // background — global styles first, then the theme's settings.
1208 + $statedText = $gsText !== '' ? $gsText : (string)Arr::get($settings, 'text', '');
1209 +
1210 + if ($statedText !== '') {
1211 + $text = $statedText;
1212 + }
1213 +
870 1214 $accent = (string)Arr::get($settings, 'accent', '');
871 1215
872 1216 if ($accent === '') {
873 1217 $accent = self::value(Arr::get($map, 'accent', ''));
@@ -872,14 +1216,13 @@
872 1216 if ($accent === '') {
873 1217 $accent = self::value(Arr::get($map, 'accent', ''));
874 1218 }
875 1219
876 - // The button follows the same law as the page pair: what the owner
877 - // set in the site editor (Styles → Buttons) outranks every guess the
878 - // palette could offer — but only led by its background. A background
879 - // brings its own text partner, or gets one measured against it in
880 - // resolve(); a lone text fragment is dropped rather than printed onto
881 - // a background it was never chosen for.
1220 + // What the owner set in the site editor (Styles → Buttons) outranks
1221 + // every guess the palette could offer, then the theme's settings. A
1222 + // background brings its own text, or gets one measured against it in
1223 + // resolve(); a text the owner set alone is worn as given on whatever
1224 + // background the button otherwise gets.
882 1225 $button = self::buttonGlobals();
883 1226 $buttonText = '';
884 1227
885 1228 if ($button['background'] !== '') {
@@ -896,8 +1239,10 @@
896 1239 // Not a default — the theme's own accent, which is what a
897 1240 // theme that names one button colour almost always means by it.
898 1241 $buttonBg = $accent;
899 1242 }
1243 +
1244 + $buttonText = $button['text'] !== '' ? $button['text'] : (string)Arr::get($settings, 'button_text', '');
900 1245 }
901 1246
902 1247 return [
903 1248 'surface' => $surface,
@@ -972,37 +1317,41 @@
972 1317 $derived['border'] = (string)$settings['border'];
973 1318 }
974 1319
975 1320 // Contrast needs a real colour to measure against for the same reason.
976 - // And the button is owned as a pair or not at all: a background whose
977 - // value cannot be measured (a bare reference — the theme resolves it on
978 - // the page, and the store owner can recolour it to anything) is one no
979 - // readable text can be paired with here, so neither half is written and
980 - // the stylesheets' own paired fallback styles the button instead. Text
981 - // 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.
1321 + // Colours the owner chose (the site editor's, or a theme settings
1322 + // reader's) are worn exactly as given, at any contrast; only a missing
1323 + // one is measured. A background nobody chose whose value cannot be
1324 + // measured (a bare palette reference) is not worn at all: no readable
1325 + // text can be paired with it here, so the stylesheets' own fallback
1326 + // styles the button instead. One the owner chose is still worn, with
1327 + // nothing invented beside it.
984 1328 $buttonBgHex = self::measurable($buttonBg);
985 - $ownText = self::statedTextOn((string)Arr::get($anchors, 'button_text', ''), $buttonBgHex);
1329 + $ownText = (string)Arr::get($anchors, 'button_text', '');
1330 + $stated = self::buttonGlobals();
1331 + $buttonStated = $stated['background'] !== '' || Arr::get($settings, 'button_bg', '') !== '';
986 1332
987 1333 if ($buttonBgHex !== '') {
988 1334 $derived['button_text'] = $ownText !== '' ? $ownText : ColorMath::readableText($buttonBgHex);
989 1335 } else {
990 - $anchors['button_bg'] = '';
991 - $derived['button_text'] = '';
1336 + if (!$buttonStated) {
1337 + $anchors['button_bg'] = '';
1338 + }
1339 +
1340 + $derived['button_text'] = $ownText;
992 1341 }
993 1342
994 1343 // The hover follows the button it belongs to: unwritten when the button
995 1344 // is. The site editor's `:hover` pair outranks any guess; otherwise the
996 1345 // 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.
1346 + // lighter for a dark one, and never for a button that cannot be
1347 + // measured. Hover text given with it is worn as given; otherwise the
1348 + // resting text carries over while it still reads (WCAG 4.5:1), and a
1349 + // partner is measured when it does not.
1000 1350 $derived['button_hover_bg'] = '';
1001 1351 $derived['button_hover_text'] = '';
1002 1352
1003 - if ($buttonBgHex !== '') {
1004 - $stated = self::buttonGlobals();
1353 + if ($buttonBgHex !== '' || $buttonStated) {
1005 1354 $hoverBg = $stated['hover_background'];
1006 1355 $hoverText = $stated['hover_text'];
1007 1356
1008 1357 // The theme settings' hover belongs to the theme settings' button:
@@ -1017,14 +1366,13 @@
1017 1366 $hoverText = (string)Arr::get($settings, 'button_hover_text', '');
1018 1367 }
1019 1368 }
1020 1369
1021 - if ($hoverBg === '') {
1370 + if ($hoverBg === '' && $buttonBgHex !== '') {
1022 1371 $hoverBg = ColorMath::shiftFromItself($buttonBgHex, 12);
1023 1372 }
1024 1373
1025 1374 $hoverBgHex = self::measurable($hoverBg);
1026 - $hoverText = self::statedTextOn($hoverText, $hoverBgHex);
1027 1375
1028 1376 if ($hoverText === '' && $hoverBgHex !== '') {
1029 1377 $hoverText = self::hoverTextFor($hoverBgHex, (string)$derived['button_text']);
1030 1378 }
@@ -1068,37 +1416,8 @@
1068 1416 */
1069 1417 self::$cachedRoles = apply_filters('fluent_cart/theme/roles', array_merge($anchors, $derived));
1070 1418
1071 1419 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 1420 }
1102 1421
1103 1422 /**
1104 1423 * The text for a hover background nobody gave a text: the button's