From dbf48a692d3c0d766e8187d5c8276720bebbe9d1 Mon Sep 17 00:00:00 2001 From: Malin Date: Thu, 5 Mar 2026 08:01:33 +0100 Subject: [PATCH] feat: initial release of CommerceKit Floating Cart add-on v1.0.0 --- assets/css/floating-cart.css | 124 ++++++++++++++++++++++++ assets/js/floating-cart.js | 147 ++++++++++++++++++++++++++++ cgkit-floating-cart.php | 183 +++++++++++++++++++++++++++++++++++ 3 files changed, 454 insertions(+) create mode 100644 assets/css/floating-cart.css create mode 100644 assets/js/floating-cart.js create mode 100644 cgkit-floating-cart.php diff --git a/assets/css/floating-cart.css b/assets/css/floating-cart.css new file mode 100644 index 0000000..2cab35f --- /dev/null +++ b/assets/css/floating-cart.css @@ -0,0 +1,124 @@ +/* ========================================================================== + CommerceKit Floating Cart + ========================================================================== */ + +/* Container – fixed to bottom-right, above most theme layers */ +#cgkit-floating-cart { + position: fixed; + bottom: 28px; + right: 28px; + z-index: 99998; /* below most modals/overlays but above normal content */ +} + +/* The circle button */ +.cgkit-floating-cart__btn { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + padding: 0; + border: none; + border-radius: 50%; + background-color: #2c2c2c; + color: #ffffff; + cursor: pointer; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.22); + transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease; + /* Prevent tap-highlight on mobile */ + -webkit-tap-highlight-color: transparent; +} + +.cgkit-floating-cart__btn:hover, +.cgkit-floating-cart__btn:focus-visible { + background-color: #444444; + transform: scale(1.08); + box-shadow: 0 6px 22px rgba(0, 0, 0, 0.3); + outline: none; +} + +.cgkit-floating-cart__btn:active { + transform: scale(0.96); +} + +/* Cart icon SVG */ +.cgkit-floating-cart__icon { + display: block; + width: 22px; + height: 22px; + pointer-events: none; +} + +/* Item-count badge */ +.cgkit-floating-cart__count { + position: absolute; + top: -4px; + right: -4px; + min-width: 20px; + height: 20px; + padding: 0 4px; + border-radius: 10px; + background-color: #e84040; + color: #ffffff; + font-size: 11px; + font-weight: 700; + line-height: 20px; + text-align: center; + pointer-events: none; + transition: transform 0.15s ease; +} + +/* Hide badge when cart is empty */ +.cgkit-floating-cart__count--hidden { + display: none; +} + +/* "Bump" animation when count changes */ +.cgkit-floating-cart__count--bump { + animation: cgkit-fc-bump 0.3s ease; +} + +@keyframes cgkit-fc-bump { + 0% { transform: scale(1); } + 50% { transform: scale(1.4); } + 100% { transform: scale(1); } +} + +/* "Pulse" ring that briefly appears after add-to-cart */ +.cgkit-floating-cart__btn::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + border: 2px solid #2c2c2c; + opacity: 0; + transform: scale(1); +} + +.cgkit-floating-cart__btn.cgkit-fc--pulse::after { + animation: cgkit-fc-pulse 0.5s ease-out forwards; +} + +@keyframes cgkit-fc-pulse { + 0% { opacity: 0.6; transform: scale(1); } + 100% { opacity: 0; transform: scale(1.6); } +} + +/* Responsive: shrink slightly on small screens */ +@media (max-width: 480px) { + #cgkit-floating-cart { + bottom: 18px; + right: 18px; + } + + .cgkit-floating-cart__btn { + width: 50px; + height: 50px; + } + + .cgkit-floating-cart__icon { + width: 20px; + height: 20px; + } +} diff --git a/assets/js/floating-cart.js b/assets/js/floating-cart.js new file mode 100644 index 0000000..6ffee7d --- /dev/null +++ b/assets/js/floating-cart.js @@ -0,0 +1,147 @@ +/** + * CommerceKit Floating Cart – frontend script + * + * Responsibilities: + * 1. Open the CommerceKit / Shoptimizer minicart when the floating button is clicked. + * 2. Automatically open the minicart after a successful add-to-cart (AJAX or standard). + * 3. Play a small badge-bump animation when the item count changes. + * + * All selectors and feature flags come from the `cgkitFC` object localised by PHP. + */ +( function ( $ ) { + 'use strict'; + + /* ----------------------------------------------------------------------- + * Module + * --------------------------------------------------------------------- */ + var FloatingCart = { + + $btn: null, + $count: null, + prevCount: 0, + + /** Bootstrap */ + init: function () { + if ( typeof cgkitFC === 'undefined' ) { + return; + } + + this.$btn = $( '#cgkit-floating-cart .cgkit-floating-cart__btn' ); + this.$count = $( '#cgkit-fc-count' ); + + if ( ! this.$btn.length ) { + return; + } + + this.prevCount = parseInt( this.$count.text(), 10 ) || 0; + + this.bindEvents(); + }, + + /** Attach event listeners */ + bindEvents: function () { + var self = this; + + // --- Floating button click → open minicart ----------------------- + this.$btn.on( 'click', function ( e ) { + e.preventDefault(); + self.openMinicart(); + } ); + + // --- Auto-open after AJAX add-to-cart ---------------------------- + if ( cgkitFC.autoOpen === 'yes' ) { + $( document.body ).on( 'added_to_cart', function () { + setTimeout( function () { + self.openMinicart(); + self.pulseBtn(); + }, parseInt( cgkitFC.autoOpenDelay, 10 ) || 400 ); + } ); + } + + // --- Badge bump when WooCommerce refreshes fragments ------------- + $( document.body ).on( + 'wc_fragments_refreshed wc_fragments_loaded', + function () { + self.maybeBumpBadge(); + } + ); + }, + + /** + * Trigger the theme's minicart open mechanism. + * + * Strategy: + * 1. Try clicking the first element matching the configured selector. + * 2. If that element has no jQuery click handlers registered, dispatch + * a native 'click' so themes that use addEventListener also fire. + * 3. Last resort: navigate to the cart URL. + */ + openMinicart: function () { + var selectors = ( cgkitFC.minicartTrigger || '' ).split( ',' ); + var $trigger = null; + + for ( var i = 0; i < selectors.length; i++ ) { + var $el = $( $.trim( selectors[ i ] ) ).first(); + if ( $el.length ) { + $trigger = $el; + break; + } + } + + if ( $trigger && $trigger.length ) { + // Fire both jQuery and native click so all listeners catch it. + $trigger.trigger( 'click' ); + var nativeEl = $trigger.get( 0 ); + if ( nativeEl && typeof nativeEl.click === 'function' ) { + nativeEl.click(); + } + return; + } + + // No trigger found → fall back to cart page + if ( cgkitFC.cartUrl ) { + window.location.href = cgkitFC.cartUrl; + } + }, + + /** + * Add a pulse ring on the button after add-to-cart. + */ + pulseBtn: function () { + var self = this; + this.$btn.addClass( 'cgkit-fc--pulse' ); + setTimeout( function () { + self.$btn.removeClass( 'cgkit-fc--pulse' ); + }, 600 ); + }, + + /** + * If the item count increased, animate the badge. + */ + maybeBumpBadge: function () { + var $freshCount = $( '#cgkit-fc-count' ); + // Re-query in case the fragment replaced the element. + if ( ! $freshCount.length ) { + return; + } + this.$count = $freshCount; + + var newCount = parseInt( this.$count.text(), 10 ) || 0; + if ( newCount !== this.prevCount ) { + this.prevCount = newCount; + this.$count.removeClass( 'cgkit-floating-cart__count--bump' ); + // Force reflow so removing + re-adding the class restarts the animation. + void this.$count[ 0 ].offsetWidth; // eslint-disable-line no-unused-expressions + this.$count.addClass( 'cgkit-floating-cart__count--bump' ); + } + } + }; + + /* ----------------------------------------------------------------------- + * Boot + * --------------------------------------------------------------------- */ + $( document ).ready( function () { + FloatingCart.init(); + } ); + +} )( jQuery ); diff --git a/cgkit-floating-cart.php b/cgkit-floating-cart.php new file mode 100644 index 0000000..97a8c22 --- /dev/null +++ b/cgkit-floating-cart.php @@ -0,0 +1,183 @@ + $trigger, + 'autoOpen' => $auto_open ? 'yes' : 'no', + 'autoOpenDelay' => $delay, + 'cartUrl' => wc_get_cart_url(), + ] ); + } + + /** + * Output the floating cart button HTML in the footer. + */ + public function render_floating_cart(): void { + $count = WC()->cart ? WC()->cart->get_cart_contents_count() : 0; + ?> + + cart ? WC()->cart->get_cart_contents_count() : 0; + $fragments['#cgkit-fc-count'] = $this->count_badge_html( $count ); + return $fragments; + } + + // ------------------------------------------------------------------------- + // Private helpers + // ------------------------------------------------------------------------- + + /** + * Return the count badge HTML (targeted by fragments). + */ + private function count_badge_html( int $count ): string { + $hidden = 0 === $count ? ' cgkit-floating-cart__count--hidden' : ''; + return sprintf( + '', + esc_attr( $hidden ), + esc_html( $count > 0 ? $count : '' ) + ); + } + + /** + * Shopping-cart SVG icon (Feather icons style, matches CommerceKit aesthetic). + */ + private function cart_icon_svg(): string { + return ''; + } +} + +/** + * Bootstrap after WooCommerce is loaded so WC() is available. + */ +add_action( 'woocommerce_loaded', function () { + CommerceKit_Floating_Cart::get_instance(); +} );