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/FrontendTheme.php +243 -22 1.6.5 → 1.7.1 View file →
@@ -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
@@ -74,10 +74,10 @@
74 74 * A named callback on the public filter rather than a call inside the
75 75 * gateway: the payment module stays unaware of the theme service, and
76 76 * removing or replacing FluentCart's contribution is a *_filter() call.
77 77 *
78 - * A default-filler, not an owner: the palette supplies its theme and its
79 - * three colour variables, and everything else — a customised seed, or
78 + * A default-filler, not an owner: the palette supplies its theme, its
79 + * colour variables and its tab rules, and everything else — a customised seed, or
80 80 * what an earlier listener added (labels, rules, extra variables) —
81 81 * survives. The theme is only taken over from the plain default, since
82 82 * an explicit theme choice is a choice. With no opinion (default source,
83 83 * nothing measurable), the incoming value passes through unchanged.
@@ -104,8 +104,24 @@
104 104 (array)Arr::get($appearance, 'variables', []),
105 105 $palette['variables']
106 106 );
107 107
108 + // Per selector: the palette sets its properties, and every other
109 + // selector and property an earlier listener wrote survives. Selectors
110 + // start with a dot, so they are indexed directly, never via Arr::get().
111 + $rules = (array)Arr::get($appearance, 'rules', []);
112 +
113 + foreach ((array)Arr::get($palette, 'rules', []) as $selector => $properties) {
114 + $rules[$selector] = array_merge(
115 + isset($rules[$selector]) ? (array)$rules[$selector] : [],
116 + $properties
117 + );
118 + }
119 +
120 + if ($rules) {
121 + $appearance['rules'] = $rules;
122 + }
123 +
108 124 return $appearance;
109 125 }
110 126
111 127 /**
@@ -124,9 +140,12 @@
124 140 *
125 141 * Seeds the `fluent_cart/stripe_appearance` filter's default — a listener
126 142 * still overrides everything here.
127 143 *
128 - * @return array Stripe appearance config: ['theme' => ..., 'variables' => ...].
144 + * The button pair reaches Stripe on its own, without the input pair: it
145 + * is the accent, and it fills the selected payment-method tab.
146 + *
147 + * @return array Stripe appearance config: ['theme' => ..., 'variables' => ..., 'rules' => ...].
129 148 */
130 149 public static function stripeAppearance(): array
131 150 {
132 151 $appearance = ['theme' => 'stripe'];
@@ -139,28 +158,51 @@
139 158
140 159 $inputBg = (string)Arr::get($colors, 'input_bg_color', '');
141 160 $inputText = (string)Arr::get($colors, 'input_text_color', '');
142 161
143 - if ($inputBg === '' || $inputText === '') {
144 - return $appearance;
145 - }
146 -
147 - $appearance = [
162 + if ($inputBg !== '' && $inputText !== '') {
148 163 // readableOn() picks white on a dark surface — which is exactly
149 164 // when Stripe should start from night instead of its light theme.
150 - 'theme' => ColorMath::readableOn($inputBg) === '#ffffff' ? 'night' : 'stripe',
151 - 'variables' => [
165 + $appearance['theme'] = ColorMath::readableOn($inputBg) === '#ffffff' ? 'night' : 'stripe';
166 + $appearance['variables'] = [
152 167 'colorBackground' => $inputBg,
153 168 'colorText' => $inputText,
154 - ],
155 - ];
169 + ];
170 + }
156 171
157 - $accent = (string)Arr::get($colors, 'primary_bg_color', '');
172 + // The accent and the selected payment-method tab follow the button,
173 + // not primary_bg_color: that is a pale surface behind active states,
174 + // and as Stripe's accent it washed the selected tab out entirely.
175 + $buttonBg = (string)Arr::get($colors, 'btn_bg_color', '');
158 176
159 - if ($accent !== '') {
160 - $appearance['variables']['colorPrimary'] = $accent;
177 + if ($buttonBg === '') {
178 + return $appearance;
161 179 }
162 180
181 + $buttonText = (string)Arr::get($colors, 'btn_text_color', '');
182 +
183 + if ($buttonText === '') {
184 + $buttonText = ColorMath::readableText($buttonBg);
185 + }
186 +
187 + $appearance['variables']['colorPrimary'] = $buttonBg;
188 +
189 + $selectedTab = [
190 + 'backgroundColor' => $buttonBg,
191 + 'borderColor' => $buttonBg,
192 + 'color' => $buttonText,
193 + ];
194 +
195 + $appearance['rules'] = [
196 + '.Tab:hover' => ['borderColor' => $buttonBg],
197 + '.Tab--selected' => $selectedTab,
198 + '.Tab--selected:hover' => $selectedTab,
199 + '.Tab--selected:focus' => $selectedTab,
200 + '.TabIcon--selected' => ['fill' => $buttonText],
201 + '.TabIcon--selected:hover' => ['fill' => $buttonText],
202 + '.TabLabel--selected' => ['color' => $buttonText],
203 + ];
204 +
163 205 return $appearance;
164 206 }
165 207
166 208 /**
@@ -269,11 +311,13 @@
269 311 }
270 312
271 313 /*
272 314 * Not escaped on output because it cannot carry anything to escape:
273 - * every property name comes from the ColorPalette registry and every
274 - * value has been through sanitize_hex_color(), so the string is only
275 - * 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;`.
276 320 */
277 321 echo '<style id="' . esc_attr(self::STYLE_ID) . '">' . $css . '</style>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
278 322 }
279 323
@@ -381,8 +425,51 @@
381 425 return preg_match('/^var\(--[A-Za-z0-9_-]+(?:, #(?:[0-9a-f]{3}|[0-9a-f]{6}))?\)$/', $value) ? $value : '';
382 426 }
383 427
384 428 /**
429 + * Give a customised button what theme inheritance would have given it.
430 + *
431 + * Customize lets the owner leave a button's text unset, and every
432 + * stylesheet then falls back to its own literal text — not always one
433 + * that reads on the background the owner did set (the product carousel's
434 + * hovered arrow fell back to a dark icon). Theme inheritance already
435 + * measures a missing partner; this does the same for the button pair and
436 + * the hover pair.
437 + *
438 + * A button background with no hover background also gets the hover
439 + * inheritance derives — the button moved 12% away from itself — with the
440 + * resting text carried over while it reads; otherwise the button hovered
441 + * in its resting colour, with no visible change. A colour the owner chose
442 + * is worn as given.
443 + *
444 + * @param array $colors Settings key => hex, as the owner set them.
445 + * @return array
446 + */
447 + protected static function withButtonPartners(array $colors): array
448 + {
449 + if (isset($colors['btn_bg_color']) && !isset($colors['btn_text_color'])) {
450 + $colors['btn_text_color'] = ColorMath::readableText($colors['btn_bg_color']);
451 + }
452 +
453 + if (isset($colors['btn_bg_color']) && !isset($colors['btn_hover_bg_color'])) {
454 + $colors['btn_hover_bg_color'] = ColorMath::shiftFromItself($colors['btn_bg_color'], 12);
455 +
456 + if (!isset($colors['btn_hover_text_color'])) {
457 + $colors['btn_hover_text_color'] = ThemePalette::hoverTextFor(
458 + $colors['btn_hover_bg_color'],
459 + $colors['btn_text_color']
460 + );
461 + }
462 + }
463 +
464 + if (isset($colors['btn_hover_bg_color']) && !isset($colors['btn_hover_text_color'])) {
465 + $colors['btn_hover_text_color'] = ColorMath::readableText($colors['btn_hover_bg_color']);
466 + }
467 +
468 + return $colors;
469 + }
470 +
471 + /**
385 472 * Every colour that will actually be written, keyed by settings key.
386 473 *
387 474 * @return array
388 475 */
@@ -390,9 +477,9 @@
390 477 {
391 478 $source = self::getSource();
392 479
393 480 if ($source === ColorPalette::SOURCE_CUSTOM) {
394 - $colors = self::getCustomColors();
481 + $colors = self::withButtonPartners(self::getCustomColors());
395 482 } elseif ($source === ColorPalette::SOURCE_THEME) {
396 483 $colors = self::getThemeColors();
397 484 } else {
398 485 $colors = [];
@@ -441,14 +528,148 @@
441 528 return $measured;
442 529 }
443 530
444 531 /**
445 - * Build the `:root` declaration block.
532 + * The radii the store owner set by hand, role => length.
446 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 + *
447 631 * @return string CSS, or '' when there is nothing to write.
448 632 */
449 633 public static function buildCss(): string
450 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 + {
451 672 $colors = self::getEffectiveColors();
452 673
453 674 if (!$colors) {
454 675 return '';
@@ -474,7 +695,7 @@
474 695 $declarations .= $property . ':' . $safe . ';';
475 696 }
476 697 }
477 698
478 - return $declarations === '' ? '' : ':root{' . $declarations . '}';
699 + return $declarations;
479 700 }
480 701 }