diff --git a/designer.html b/designer.html new file mode 100644 index 0000000..c834a41 --- /dev/null +++ b/designer.html @@ -0,0 +1,384 @@ + + + + + + SocialShareButton — Theme Designer + + + + + + + + + + + + + + +
+ + +
+ + +
+

Share Button

+
+
+
+
+ + +
+

Modal Preview (live)

+
+ +
+
+ +
+ + + + +
+ + + + + + + + diff --git a/src/social-share-designer-react.jsx b/src/social-share-designer-react.jsx new file mode 100644 index 0000000..f8900e2 --- /dev/null +++ b/src/social-share-designer-react.jsx @@ -0,0 +1,71 @@ +import { useEffect, useRef } from "react"; + +/** + * SocialShareDesigner (React wrapper) + * Wraps window.SocialShareDesigner using the same useRef + useEffect pattern + * as the existing SocialShareButton React wrapper. + * + * Usage: + * import { SocialShareButton } from "./social-share-button-react"; + * import { SocialShareDesigner } from "./social-share-designer-react"; + * + * + * + */ + +export const SocialShareDesigner = ({ + /** Pass the SocialShareButton JS instance for full control, + * or a CSS selector string for CSS-vars-only mode. */ + targetButton = null, +}) => { + const panelRef = useRef(null); + const designerRef = useRef(null); + + useEffect(() => { + if (!panelRef.current || designerRef.current) return; + + if (typeof window === "undefined" || !window.SocialShareDesigner) { + console.warn( + "[SocialShareDesigner] window.SocialShareDesigner not found. " + + "Make sure social-share-designer.js is loaded before this component mounts." + ); + return; + } + + designerRef.current = new window.SocialShareDesigner({ + panelContainer: panelRef.current, + targetButton, + }); + + return () => { + if (designerRef.current) { + designerRef.current.destroy(); + designerRef.current = null; + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // If targetButton changes (e.g. the SocialShareButton instance is replaced), + // tear down and rebuild the designer so it points at the new instance. + useEffect(() => { + if (!designerRef.current) return; + + designerRef.current.destroy(); + designerRef.current = null; + + if (!panelRef.current) return; + + if (typeof window !== "undefined" && window.SocialShareDesigner) { + designerRef.current = new window.SocialShareDesigner({ + panelContainer: panelRef.current, + targetButton, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [targetButton]); + + return
; +}; + +export default SocialShareDesigner; diff --git a/src/social-share-designer.css b/src/social-share-designer.css new file mode 100644 index 0000000..631dce5 --- /dev/null +++ b/src/social-share-designer.css @@ -0,0 +1,545 @@ +/** + * SocialShareDesigner Styles + * Designer panel for customizing SocialShareButton via CSS custom properties + */ + +/* ── Panel Container ────────────────────────────────────────── */ +.ssb-designer { + background: #1a1a1a; + color: #fff; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 13px; + width: 300px; + min-width: 280px; + border-radius: 12px; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + height: 100%; + max-height: 100vh; +} + +.ssb-designer-header { + padding: 14px 18px; + border-bottom: 1px solid #2a2a2a; + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; +} + +.ssb-designer-header h2 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #fff; + letter-spacing: 0.3px; +} + +.ssb-designer-badge { + background: #f5c518; + color: #000; + font-size: 9px; + font-weight: 700; + padding: 2px 6px; + border-radius: 8px; + letter-spacing: 0.6px; + text-transform: uppercase; +} + +/* ── Scrollable Body ────────────────────────────────────────── */ +.ssb-designer-body { + flex: 1; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #3a3a3a transparent; +} + +.ssb-designer-body::-webkit-scrollbar { + width: 4px; +} +.ssb-designer-body::-webkit-scrollbar-thumb { + background: #3a3a3a; + border-radius: 4px; +} + +/* ── Section ────────────────────────────────────────────────── */ +.ssb-section { + padding: 14px 18px; + border-top: 1px solid #2a2a2a; +} + +.ssb-section:first-child { + border-top: none; +} + +.ssb-section-label { + display: block; + text-transform: uppercase; + color: #888; + font-size: 10px; + font-weight: 600; + letter-spacing: 1.2px; + margin-bottom: 10px; +} + +/* ── Toggle Group ───────────────────────────────────────────── */ +.ssb-toggle-group { + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.ssb-toggle-group button { + flex: 1; + min-width: 0; + padding: 6px 8px; + background: #2a2a2a; + border: 1px solid #3a3a3a; + border-radius: 6px; + color: #bbb; + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; + text-align: center; +} + +.ssb-toggle-group button:hover { + background: #333; + color: #fff; +} + +.ssb-toggle-group button.active { + background: #f5c518; + border-color: #f5c518; + color: #000; + font-weight: 600; +} + +/* ── Platform List ──────────────────────────────────────────── */ +.ssb-platform-list { + display: flex; + flex-direction: column; + gap: 7px; +} + +.ssb-platform-row { + display: flex; + align-items: center; + justify-content: space-between; +} + +.ssb-platform-row-label { + font-size: 12px; + color: #ccc; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; +} + +.ssb-platform-dot { + width: 9px; + height: 9px; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; +} + +/* ── Toggle Switch ──────────────────────────────────────────── */ +.ssb-switch { + position: relative; + width: 34px; + height: 18px; + flex-shrink: 0; +} + +.ssb-switch input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +.ssb-switch-track { + position: absolute; + inset: 0; + background: #3a3a3a; + border-radius: 18px; + cursor: pointer; + transition: background 0.2s ease; +} + +.ssb-switch-track::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 12px; + height: 12px; + background: #666; + border-radius: 50%; + transition: all 0.2s ease; +} + +.ssb-switch input:checked + .ssb-switch-track { + background: #f5c518; +} + +.ssb-switch input:checked + .ssb-switch-track::after { + left: 19px; + background: #000; +} + +/* ── Color Swatches ─────────────────────────────────────────── */ +.ssb-swatch-row { + display: flex; + gap: 7px; + flex-wrap: wrap; + align-items: center; +} + +.ssb-swatch { + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; + transition: all 0.15s ease; + padding: 0; + outline: none; + flex-shrink: 0; + box-sizing: border-box; +} + +.ssb-swatch:hover { + transform: scale(1.15); +} + +.ssb-swatch.active { + border-color: #f5c518; + box-shadow: 0 0 0 2px rgba(245, 197, 24, 0.3); +} + +/* ── Inline Color Picker ────────────────────────────────────── */ +.ssb-color-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; +} + +.ssb-color-row label { + font-size: 11px; + color: #888; + min-width: 80px; + flex-shrink: 0; +} + +.ssb-color-wrap { + display: flex; + align-items: center; + gap: 6px; + background: #2a2a2a; + border: 1px solid #3a3a3a; + border-radius: 6px; + padding: 4px 8px; + flex: 1; + transition: border-color 0.15s; +} + +.ssb-color-wrap:focus-within { + border-color: #f5c518; +} + +.ssb-color-wrap input[type="color"] { + width: 20px; + height: 20px; + border: none; + padding: 0; + background: none; + cursor: pointer; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; +} + +.ssb-color-wrap input[type="text"] { + background: none; + border: none; + color: #ccc; + font-size: 11px; + width: 60px; + outline: none; + font-family: "Courier New", monospace; +} + +/* ── Gradient Toggle Row ────────────────────────────────────── */ +.ssb-gradient-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #2a2a2a; +} + +.ssb-gradient-row span { + font-size: 12px; + color: #aaa; +} + +.ssb-gradient-pickers { + display: flex; + gap: 8px; + margin-top: 10px; +} + +.ssb-gradient-pickers .ssb-color-row { + flex: 1; + flex-direction: column; + align-items: flex-start; + margin-top: 0; + gap: 5px; +} + +.ssb-gradient-pickers .ssb-color-row label { + min-width: unset; +} + +.ssb-gradient-pickers .ssb-color-wrap { + width: 100%; +} + +.ssb-gradient-preview { + height: 8px; + border-radius: 4px; + margin-top: 10px; + border: 1px solid #3a3a3a; + transition: background 0.2s; +} + +/* ── Sliders ────────────────────────────────────────────────── */ +.ssb-slider-row { + margin-bottom: 11px; +} + +.ssb-slider-row:last-child { + margin-bottom: 0; +} + +.ssb-slider-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 5px; +} + +.ssb-slider-label { + font-size: 11px; + color: #aaa; +} + +.ssb-slider-value { + font-size: 11px; + color: #f5c518; + font-weight: 600; + min-width: 38px; + text-align: right; + font-family: "Courier New", monospace; +} + +.ssb-slider-row input[type="range"] { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 4px; + background: #3a3a3a; + border-radius: 4px; + outline: none; + cursor: pointer; +} + +.ssb-slider-row input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 13px; + height: 13px; + border-radius: 50%; + background: #f5c518; + cursor: pointer; + border: 2px solid #1a1a1a; + transition: transform 0.15s; +} + +.ssb-slider-row input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.25); +} + +.ssb-slider-row input[type="range"]::-moz-range-thumb { + width: 13px; + height: 13px; + border-radius: 50%; + background: #f5c518; + cursor: pointer; + border: 2px solid #1a1a1a; +} + +/* ── Text / Select Inputs ───────────────────────────────────── */ +.ssb-input-row { + margin-bottom: 11px; +} + +.ssb-input-row:last-child { + margin-bottom: 0; +} + +.ssb-input-row label { + display: block; + font-size: 11px; + color: #aaa; + margin-bottom: 5px; +} + +.ssb-input-row input[type="text"], +.ssb-input-row select { + background: #2a2a2a; + border: 1px solid #3a3a3a; + border-radius: 6px; + color: #fff; + font-size: 12px; + padding: 7px 10px; + outline: none; + width: 100%; + transition: border-color 0.15s; + -webkit-appearance: none; + appearance: none; + box-sizing: border-box; +} + +.ssb-input-row input[type="text"]:focus, +.ssb-input-row select:focus { + border-color: #f5c518; +} + +.ssb-input-row select { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + padding-right: 26px; + cursor: pointer; +} + +/* ── Per-Platform Icon Colors ───────────────────────────────── */ +.ssb-icon-colors { + display: flex; + flex-direction: column; + gap: 8px; +} + +.ssb-icon-color-row { + display: flex; + align-items: center; + justify-content: space-between; +} + +.ssb-icon-color-label { + font-size: 12px; + color: #ccc; + display: flex; + align-items: center; + gap: 8px; +} + +/* ── Footer ─────────────────────────────────────────────────── */ +.ssb-designer-footer { + padding: 14px 18px; + border-top: 1px solid #2a2a2a; + display: flex; + gap: 8px; + background: #1a1a1a; + flex-shrink: 0; +} + +.ssb-btn-export { + flex: 1; + padding: 9px 12px; + background: #f5c518; + border: none; + border-radius: 7px; + color: #000; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; + letter-spacing: 0.2px; +} + +.ssb-btn-export:hover { + background: #ffd33d; +} + +.ssb-btn-export:active { + transform: scale(0.97); +} + +.ssb-btn-reset { + padding: 9px 12px; + background: transparent; + border: 1px solid #555; + border-radius: 7px; + color: #aaa; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; +} + +.ssb-btn-reset:hover { + border-color: #888; + color: #fff; +} + +/* ── Toast ──────────────────────────────────────────────────── */ +.ssb-toast { + position: fixed; + bottom: 30px; + left: 50%; + transform: translateX(-50%) translateY(10px); + background: #f5c518; + color: #000; + font-size: 12px; + font-weight: 600; + padding: 9px 18px; + border-radius: 18px; + z-index: 999999; + opacity: 0; + transition: all 0.25s ease; + pointer-events: none; + white-space: nowrap; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +.ssb-toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* ── Sub-label (within sections) ───────────────────────────── */ +.ssb-sub-label { + display: block; + font-size: 10px; + color: #666; + text-transform: uppercase; + letter-spacing: 0.8px; + margin-bottom: 7px; + margin-top: 11px; +} + +.ssb-sub-label:first-child { + margin-top: 0; +} diff --git a/src/social-share-designer.js b/src/social-share-designer.js new file mode 100644 index 0000000..8d5fd4a --- /dev/null +++ b/src/social-share-designer.js @@ -0,0 +1,955 @@ +/** + * SocialShareDesigner + * Visual customization panel for SocialShareButton via CSS custom properties + * @version 1.0.0 + * @license GPL-3.0 + */ + +(function (root) { + 'use strict'; + + /* ── Platform registry ───────────────────────────────────── */ + const PLATFORMS = { + whatsapp: { name: 'WhatsApp', color: '#25D366', emoji: '💬' }, + facebook: { name: 'Facebook', color: '#1877F2', emoji: 'f' }, + twitter: { name: 'X', color: '#000000', emoji: 'X' }, + linkedin: { name: 'LinkedIn', color: '#0A66C2', emoji: 'in' }, + telegram: { name: 'Telegram', color: '#0088cc', emoji: '✈' }, + reddit: { name: 'Reddit', color: '#FF4500', emoji: 'r' }, + email: { name: 'Email', color: '#7f7f7f', emoji: '✉' }, + }; + + const ALL_PLATFORMS = Object.keys(PLATFORMS); + + /* ── CSS variable defaults (mirrors social-share-button.css) ─ */ + const DEFAULTS = { + '--ssb-btn-bg': '', + '--ssb-btn-radius': '8px', + '--ssb-btn-font-size': '14px', + '--ssb-btn-font-weight': '500', + '--ssb-btn-border-width': '1px', + '--ssb-btn-border-color': 'rgba(255,255,255,0.2)', + '--ssb-btn-gradient-start': '', + '--ssb-btn-gradient-end': '', + '--ssb-modal-bg': '#282828', + '--ssb-modal-width': '540px', + '--ssb-modal-radius': '12px', + '--ssb-modal-speed': '0.3s', + '--ssb-icon-size': '56px', + '--ssb-icon-shape': '50%', + '--ssb-accent': '#3ea6ff', + '--ssb-hover-scale': '1.05', + '--ssb-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + '--ssb-shadow-intensity': '0', + }; + + /* ── Shadow map ──────────────────────────────────────────── */ + const SHADOW_MAP = { + none: 'none', + low: '0 2px 8px rgba(0,0,0,0.25)', + medium: '0 4px 16px rgba(0,0,0,0.4)', + high: '0 8px 32px rgba(0,0,0,0.65)', + }; + + /* ── Preset button colors ─────────────────────────────────── */ + const PRESET_COLORS = [ + { label: 'Default', value: '' }, + { label: 'Blue', value: '#3ea6ff' }, + { label: 'Purple', value: '#764ba2' }, + { label: 'Green', value: '#25D366' }, + { label: 'Red', value: '#FF4500' }, + { label: 'Orange', value: '#f5c518' }, + { label: 'Dark', value: '#1a1a1a' }, + ]; + + /* ================================================================ + SocialShareDesigner + ================================================================ */ + class SocialShareDesigner { + constructor(options = {}) { + this._panelContainer = + typeof options.panelContainer === 'string' + ? document.querySelector(options.panelContainer) + : options.panelContainer; + + /* targetButton: string selector → CSS-vars only + instance → full control */ + this._targetSelector = + typeof options.targetButton === 'string' ? options.targetButton : null; + this._instance = + typeof options.targetButton === 'object' && options.targetButton !== null + ? options.targetButton + : null; + + /* Internal state */ + this._state = { + theme: 'dark', + buttonStyle: 'default', + platforms: [...ALL_PLATFORMS], + buttonColor: '', + gradientEnabled: false, + gradientStart: '#667eea', + gradientEnd: '#764ba2', + platformColors: {}, // { whatsapp: '#ff0000', ... } + }; + + this._modifiedVars = {}; // tracks which CSS vars were changed + this._observer = null; + + if (!this._panelContainer) { + console.warn('[SocialShareDesigner] panelContainer not found.'); + return; + } + + this._render(); + this._setupMutationObserver(); + } + + /* ── CSS var helpers ───────────────────────────────────── */ + _setCSSVar(name, value) { + if (value === '' || value === null || value === undefined) { + document.documentElement.style.removeProperty(name); + delete this._modifiedVars[name]; + } else { + document.documentElement.style.setProperty(name, value); + this._modifiedVars[name] = value; + } + + /* Also set background directly on the button element for gradient/solid conflict */ + if (name === '--ssb-btn-bg') { + const el = this._getButtonEl(); + if (el) { + if (value) { + el.style.setProperty('background', value); + } else { + el.style.removeProperty('background'); + } + } + } + } + + _resetAllVars() { + Object.keys(this._modifiedVars).forEach(name => { + document.documentElement.style.removeProperty(name); + }); + this._modifiedVars = {}; + + /* Remove direct element background override */ + const el = this._getButtonEl(); + if (el) el.style.removeProperty('background'); + } + + /* ── DOM helpers ───────────────────────────────────────── */ + _getButtonEl() { + if (this._targetSelector) { + const container = document.querySelector(this._targetSelector); + return container ? container.querySelector('.social-share-btn') : null; + } + if (this._instance && this._instance.button) return this._instance.button; + return null; + } + + _getOverlayEl() { + if (this._instance && this._instance.modal) return this._instance.modal; + return document.querySelector('.social-share-modal-overlay'); + } + + _getPlatformsContainerEl() { + const overlay = this._getOverlayEl(); + return overlay ? overlay.querySelector('.social-share-platforms') : null; + } + + /* ── MutationObserver for per-platform colors ──────────── */ + _setupMutationObserver() { + const overlay = this._getOverlayEl(); + if (!overlay) return; + + this._observer = new MutationObserver(() => { + if (overlay.classList.contains('active')) { + this._applyPlatformColors(); + } + }); + this._observer.observe(overlay, { attributes: true, attributeFilter: ['class'] }); + } + + _applyPlatformColors() { + const overlay = this._getOverlayEl(); + if (!overlay) return; + Object.entries(this._state.platformColors).forEach(([platform, color]) => { + const icon = overlay.querySelector( + `.social-share-platform-btn[data-platform="${platform}"] .social-share-platform-icon` + ); + if (icon) icon.style.backgroundColor = color; + }); + } + + /* ── Instance control helpers ─────────────────────────── */ + _applyTheme(theme) { + this._state.theme = theme; + + /* Update real modal overlay */ + const overlay = this._getOverlayEl(); + if (overlay) { + overlay.className = overlay.className + .replace(/\b(light|dark)\b/g, '') + .trim() + ' ' + theme; + } + + /* Update preview overlay */ + const previewOverlay = document.getElementById('ssb-preview-overlay'); + if (previewOverlay) { + previewOverlay.className = 'social-share-modal-overlay ' + theme; + } + + /* If we have a full instance, also update its options record */ + if (this._instance) { + this._instance.options.theme = theme; + } + } + + _buildPlatformHTML(activePlatforms) { + return activePlatforms + .filter(p => PLATFORMS[p]) + .map(p => { + const { name, color, emoji } = PLATFORMS[p]; + const customColor = this._state.platformColors[p] || color; + return ` + `; + }) + .join(''); + } + + _applyPlatforms(activePlatforms) { + this._state.platforms = activePlatforms; + + /* Update preview */ + const previewPlatforms = document.getElementById('ssb-preview-platforms'); + if (previewPlatforms) { + previewPlatforms.innerHTML = this._buildPlatformHTML(activePlatforms); + } + + /* Update real modal */ + const container = this._getPlatformsContainerEl(); + if (container) { + container.innerHTML = this._buildPlatformHTML(activePlatforms); + /* Re-attach share listeners on real modal buttons */ + container.querySelectorAll('.social-share-platform-btn').forEach(btn => { + btn.addEventListener('click', () => { + const platform = btn.dataset.platform; + if (this._instance) { + this._instance.share(platform); + } + }); + }); + } + } + + /* ── Toast notification ────────────────────────────────── */ + _showToast(msg) { + let toast = document.getElementById('ssb-designer-toast'); + if (!toast) { + toast = document.createElement('div'); + toast.id = 'ssb-designer-toast'; + toast.className = 'ssb-toast'; + document.body.appendChild(toast); + } + toast.textContent = msg; + toast.classList.add('show'); + clearTimeout(this._toastTimer); + this._toastTimer = setTimeout(() => toast.classList.remove('show'), 2000); + } + + /* ── Export theme ──────────────────────────────────────── */ + _exportTheme() { + const entries = Object.entries(this._modifiedVars); + if (!entries.length) { + this._showToast('No custom vars set yet'); + return; + } + const lines = entries.map(([k, v]) => ` ${k}: ${v};`).join('\n'); + const css = `:root {\n${lines}\n}`; + + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(css).then(() => this._showToast('Copied to clipboard!')); + } else { + const ta = document.createElement('textarea'); + ta.value = css; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + this._showToast('Copied to clipboard!'); + } + } + + /* ── Reset ─────────────────────────────────────────────── */ + _reset() { + this._resetAllVars(); + this._state.platformColors = {}; + + /* Reset state */ + this._state.theme = 'dark'; + this._state.buttonStyle = 'default'; + this._state.platforms = [...ALL_PLATFORMS]; + this._state.buttonColor = ''; + this._state.gradientEnabled = false; + this._state.gradientStart = '#667eea'; + this._state.gradientEnd = '#764ba2'; + + /* Re-apply theme + platforms */ + this._applyTheme('dark'); + this._applyPlatforms([...ALL_PLATFORMS]); + + /* Reset real modal to dark */ + const overlay = this._getOverlayEl(); + if (overlay) overlay.className = 'social-share-modal-overlay dark'; + + /* Re-render panel so controls reflect defaults */ + this._render(); + this._setupMutationObserver(); + } + + /* ================================================================ + RENDER + ================================================================ */ + _render() { + const s = this._state; + this._panelContainer.innerHTML = ''; + + const panel = document.createElement('div'); + panel.className = 'ssb-designer'; + panel.innerHTML = this._buildPanelHTML(s); + this._panelContainer.appendChild(panel); + + this._bindEvents(panel); + } + + _buildPanelHTML(s) { + return ` +
+

Theme Designer

+ LIVE +
+ +
+ + +
+ +
+ + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ +
+ ${ALL_PLATFORMS.map(p => { + const active = s.platforms.includes(p); + return ` +
+ + +
`; + }).join('')} +
+
+ + +
+ +
+ ${PRESET_COLORS.map(c => ` + + `).join('')} +
+
+ +
+ + +
+
+ + +
+ Gradient + +
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+ + +
+ + +
+
+ Border Radius + 8px +
+ +
+ +
+
+ Border Width + 1px +
+ +
+ +
+ +
+ + +
+
+ +
+
+ Font Size + 14px +
+ +
+ + Font Weight +
+ + + + +
+ +
+ + +
+
+ + +
+ + +
+ +
+ + +
+
+ +
+
+ Width + 540px +
+ +
+ + Position +
+ + +
+ +
+
+ Animation Speed + 0.3s +
+ +
+
+ + +
+ + +
+
+ Size + 56px +
+ +
+ + Shape +
+ + +
+ + Per-Platform Color +
+ ${this._buildIconColorRows(s.platforms, s.platformColors)} +
+
+ + +
+ + +
+ + +
+ +
+
+ Hover Scale + 1.05 +
+ +
+ + Shadow Intensity +
+ + + + +
+ +
+ +
+ + +
+
+
+ +
+ + + `; + } + + _buildIconColorRows(activePlatforms, platformColors) { + return activePlatforms + .filter(p => PLATFORMS[p]) + .map(p => { + const current = platformColors[p] || PLATFORMS[p].color; + return ` +
+ + + ${PLATFORMS[p].name} + +
+ + +
+
`; + }) + .join(''); + } + + /* ================================================================ + EVENT BINDING + ================================================================ */ + _bindEvents(panel) { + const $ = id => document.getElementById(id); + + /* ── Section 1: Theme ─────────────────────────────────── */ + panel.querySelector('[data-section="theme"]') + .querySelectorAll('button[data-theme]') + .forEach(btn => { + btn.addEventListener('click', () => { + panel.querySelectorAll('[data-theme]').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + this._applyTheme(btn.dataset.theme); + }); + }); + + /* ── Section 2: Button Style ──────────────────────────── */ + panel.querySelector('[data-section="buttonstyle"]') + .querySelectorAll('button[data-style]') + .forEach(btn => { + btn.addEventListener('click', () => { + panel.querySelectorAll('[data-style]').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + this._state.buttonStyle = btn.dataset.style; + const el = this._getButtonEl(); + if (el) { + el.className = el.className + .replace(/\b(default|primary|compact|icon-only)\b/g, '') + .trim(); + if (btn.dataset.style !== 'default') el.classList.add(btn.dataset.style); + } + if (this._instance) { + this._instance.options.buttonStyle = btn.dataset.style; + } + }); + }); + + /* ── Section 3: Platforms ─────────────────────────────── */ + panel.querySelectorAll('input[data-platform]').forEach(chk => { + chk.addEventListener('change', () => { + const active = Array.from(panel.querySelectorAll('input[data-platform]')) + .filter(c => c.checked) + .map(c => c.dataset.platform); + this._applyPlatforms(active); + /* Refresh icon-color rows */ + const iconColors = $('ssb-icon-colors'); + if (iconColors) { + iconColors.innerHTML = this._buildIconColorRows(active, this._state.platformColors); + this._bindIconColorEvents(panel); + } + }); + }); + + /* ── Section 4: Button Color ──────────────────────────── */ + /* Preset swatches */ + panel.querySelectorAll('.ssb-swatch[data-preset]').forEach(sw => { + sw.addEventListener('click', () => { + const val = sw.dataset.preset; + this._state.buttonColor = val; + this._state.gradientEnabled = false; + $('ssb-gradient-toggle').checked = false; + $('ssb-gradient-pickers').style.display = 'none'; + panel.querySelectorAll('.ssb-swatch').forEach(s => s.classList.remove('active')); + sw.classList.add('active'); + $('ssb-btn-color-text').value = val; + if (val) $('ssb-btn-color-picker').value = val; + this._setCSSVar('--ssb-btn-bg', val || ''); + this._setCSSVar('--ssb-btn-gradient-start', ''); + this._setCSSVar('--ssb-btn-gradient-end', ''); + }); + }); + + /* Custom color picker */ + const syncBtnColor = (value) => { + if (!value) return; + this._state.buttonColor = value; + this._state.gradientEnabled = false; + $('ssb-gradient-toggle').checked = false; + $('ssb-gradient-pickers').style.display = 'none'; + $('ssb-btn-color-picker').value = value; + $('ssb-btn-color-text').value = value; + panel.querySelectorAll('.ssb-swatch').forEach(s => s.classList.remove('active')); + this._setCSSVar('--ssb-btn-bg', value); + this._setCSSVar('--ssb-btn-gradient-start', ''); + this._setCSSVar('--ssb-btn-gradient-end', ''); + }; + + $('ssb-btn-color-picker').addEventListener('input', e => syncBtnColor(e.target.value)); + $('ssb-btn-color-text').addEventListener('change', e => { + const v = e.target.value.trim(); + if (v) syncBtnColor(v); + }); + + /* Gradient toggle */ + $('ssb-gradient-toggle').addEventListener('change', e => { + this._state.gradientEnabled = e.target.checked; + $('ssb-gradient-pickers').style.display = e.target.checked ? '' : 'none'; + if (e.target.checked) { + this._applyGradient(); + } else { + this._setCSSVar('--ssb-btn-gradient-start', ''); + this._setCSSVar('--ssb-btn-gradient-end', ''); + this._setCSSVar('--ssb-btn-bg', this._state.buttonColor || ''); + } + }); + + /* Gradient pickers */ + const applyGrad = () => this._applyGradient(); + const syncGradStart = v => { + if (!v) return; + this._state.gradientStart = v; + $('ssb-grad-start-picker').value = v; + $('ssb-grad-start-text').value = v; + applyGrad(); + }; + const syncGradEnd = v => { + if (!v) return; + this._state.gradientEnd = v; + $('ssb-grad-end-picker').value = v; + $('ssb-grad-end-text').value = v; + applyGrad(); + }; + + $('ssb-grad-start-picker').addEventListener('input', e => syncGradStart(e.target.value)); + $('ssb-grad-start-text').addEventListener('change', e => syncGradStart(e.target.value.trim())); + $('ssb-grad-end-picker').addEventListener('input', e => syncGradEnd(e.target.value)); + $('ssb-grad-end-text').addEventListener('change', e => syncGradEnd(e.target.value.trim())); + + /* ── Section 5: Button Shape ──────────────────────────── */ + this._bindSlider(panel, 'ssb-btn-radius', 'val-btn-radius', v => v + 'px', v => this._setCSSVar('--ssb-btn-radius', v + 'px')); + this._bindSlider(panel, 'ssb-btn-border-width','val-btn-border-width', v => v + 'px', v => this._setCSSVar('--ssb-btn-border-width', v + 'px')); + this._bindSlider(panel, 'ssb-btn-font-size', 'val-btn-font-size', v => v + 'px', v => this._setCSSVar('--ssb-btn-font-size', v + 'px')); + + const syncBorderColor = v => { + if (!v) return; + $('ssb-border-color-picker').value = v.startsWith('#') ? v : '#ffffff'; + $('ssb-border-color-text').value = v; + this._setCSSVar('--ssb-btn-border-color', v); + }; + $('ssb-border-color-picker').addEventListener('input', e => syncBorderColor(e.target.value)); + $('ssb-border-color-text').addEventListener('change', e => syncBorderColor(e.target.value.trim())); + + /* Font weight toggle */ + panel.querySelectorAll('#ssb-font-weight-group button[data-fw]').forEach(btn => { + btn.addEventListener('click', () => { + panel.querySelectorAll('#ssb-font-weight-group button').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + this._setCSSVar('--ssb-btn-font-weight', btn.dataset.fw); + }); + }); + + /* Button label */ + $('ssb-btn-label').addEventListener('input', e => { + const val = e.target.value || 'Share'; + const el = this._getButtonEl(); + if (el) { + const span = el.querySelector('span'); + if (span) span.textContent = val; + } + /* Update preview button label */ + const previewBtn = document.getElementById('ssb-preview-btn-label'); + if (previewBtn) previewBtn.textContent = val; + if (this._instance) this._instance.options.buttonText = val; + }); + + /* ── Section 6: Modal ─────────────────────────────────── */ + const syncModalBg = v => { + if (!v) return; + $('ssb-modal-bg-picker').value = v.startsWith('#') ? v : '#282828'; + $('ssb-modal-bg-text').value = v; + this._setCSSVar('--ssb-modal-bg', v); + /* live update preview card bg */ + const card = document.getElementById('ssb-preview-card'); + if (card) card.style.background = v; + }; + $('ssb-modal-bg-picker').addEventListener('input', e => syncModalBg(e.target.value)); + $('ssb-modal-bg-text').addEventListener('change', e => syncModalBg(e.target.value.trim())); + + this._bindSlider(panel, 'ssb-modal-width', 'val-modal-width', v => v + 'px', v => { + this._setCSSVar('--ssb-modal-width', v + 'px'); + const card = document.getElementById('ssb-preview-card'); + if (card) card.style.maxWidth = v + 'px'; + }); + + this._bindSlider(panel, 'ssb-modal-speed', 'val-modal-speed', v => parseFloat(v).toFixed(2) + 's', + v => this._setCSSVar('--ssb-modal-speed', parseFloat(v).toFixed(2) + 's')); + + panel.querySelectorAll('#ssb-modal-position-group button[data-pos]').forEach(btn => { + btn.addEventListener('click', () => { + panel.querySelectorAll('#ssb-modal-position-group button').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + const pos = btn.dataset.pos; + if (this._instance) { + this._instance.options.modalPosition = pos; + const content = this._getOverlayEl() + ? this._getOverlayEl().querySelector('.social-share-modal-content') + : null; + if (content) { + content.classList.remove('center', 'bottom'); + content.classList.add(pos); + } + } + const previewCard = document.getElementById('ssb-preview-card'); + if (previewCard) { + previewCard.classList.remove('center', 'bottom'); + previewCard.classList.add(pos); + } + }); + }); + + /* ── Section 7: Platform Icons ────────────────────────── */ + this._bindSlider(panel, 'ssb-icon-size', 'val-icon-size', v => v + 'px', v => { + this._setCSSVar('--ssb-icon-size', v + 'px'); + /* Also update preview icons directly */ + document.querySelectorAll('.social-share-platform-icon').forEach(icon => { + icon.style.width = v + 'px'; + icon.style.height = v + 'px'; + }); + }); + + panel.querySelectorAll('#ssb-icon-shape-group button[data-shape]').forEach(btn => { + btn.addEventListener('click', () => { + panel.querySelectorAll('#ssb-icon-shape-group button').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + this._setCSSVar('--ssb-icon-shape', btn.dataset.shape); + document.querySelectorAll('.social-share-platform-icon').forEach(icon => { + icon.style.borderRadius = btn.dataset.shape; + }); + }); + }); + + this._bindIconColorEvents(panel); + + /* ── Section 8: Advanced ──────────────────────────────── */ + $('ssb-font-family').addEventListener('change', e => { + this._setCSSVar('--ssb-font-family', e.target.value); + }); + + this._bindSlider(panel, 'ssb-hover-scale', 'val-hover-scale', + v => parseFloat(v).toFixed(2), + v => this._setCSSVar('--ssb-hover-scale', parseFloat(v).toFixed(2))); + + panel.querySelectorAll('#ssb-shadow-group button[data-shadow]').forEach(btn => { + btn.addEventListener('click', () => { + panel.querySelectorAll('#ssb-shadow-group button').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + this._setCSSVar('--ssb-shadow-intensity', SHADOW_MAP[btn.dataset.shadow] || 'none'); + }); + }); + + const syncAccent = v => { + if (!v) return; + $('ssb-accent-picker').value = v.startsWith('#') ? v : '#3ea6ff'; + $('ssb-accent-text').value = v; + this._setCSSVar('--ssb-accent', v); + }; + $('ssb-accent-picker').addEventListener('input', e => syncAccent(e.target.value)); + $('ssb-accent-text').addEventListener('change', e => syncAccent(e.target.value.trim())); + + /* ── Footer ───────────────────────────────────────────── */ + $('ssb-btn-export').addEventListener('click', () => this._exportTheme()); + $('ssb-btn-reset').addEventListener('click', () => this._reset()); + } + + _bindIconColorEvents(panel) { + panel.querySelectorAll('input[data-icon-platform]').forEach(picker => { + picker.addEventListener('input', e => { + const p = e.target.dataset.iconPlatform; + this._state.platformColors[p] = e.target.value; + /* sync text input */ + const textInput = panel.querySelector(`input[data-icon-color-text="${p}"]`); + if (textInput) textInput.value = e.target.value; + /* Apply to all visible icons */ + document.querySelectorAll( + `.social-share-platform-btn[data-platform="${p}"] .social-share-platform-icon` + ).forEach(icon => { icon.style.backgroundColor = e.target.value; }); + }); + }); + + panel.querySelectorAll('input[data-icon-color-text]').forEach(input => { + input.addEventListener('change', e => { + const p = e.target.dataset.iconColorText; + const v = e.target.value.trim(); + if (!v) return; + this._state.platformColors[p] = v; + const picker = panel.querySelector(`input[data-icon-platform="${p}"]`); + if (picker && v.startsWith('#')) picker.value = v; + document.querySelectorAll( + `.social-share-platform-btn[data-platform="${p}"] .social-share-platform-icon` + ).forEach(icon => { icon.style.backgroundColor = v; }); + }); + }); + } + + _bindSlider(panel, sliderId, valueId, fmt, onChange) { + const slider = document.getElementById(sliderId); + const valueEl = document.getElementById(valueId); + if (!slider || !valueEl) return; + slider.addEventListener('input', () => { + valueEl.textContent = fmt(slider.value); + onChange(slider.value); + }); + } + + _applyGradient() { + const start = this._state.gradientStart; + const end = this._state.gradientEnd; + const grad = `linear-gradient(135deg, ${start}, ${end})`; + + this._setCSSVar('--ssb-btn-gradient-start', start); + this._setCSSVar('--ssb-btn-gradient-end', end); + + /* Apply gradient directly on button */ + const el = this._getButtonEl(); + if (el) el.style.setProperty('background', grad); + + /* Also update the preview button */ + const previewBtn = document.querySelector('#ssb-preview-share-btn'); + if (previewBtn) previewBtn.style.background = grad; + + /* Update visual preview strip */ + const preview = document.getElementById('ssb-gradient-preview'); + if (preview) preview.style.background = grad; + } + + /* ── Public: destroy ───────────────────────────────────── */ + destroy() { + if (this._observer) { + this._observer.disconnect(); + this._observer = null; + } + clearTimeout(this._toastTimer); + const toast = document.getElementById('ssb-designer-toast'); + if (toast && toast.parentNode) toast.parentNode.removeChild(toast); + if (this._panelContainer) this._panelContainer.innerHTML = ''; + } + } + + /* Expose globally */ + root.SocialShareDesigner = SocialShareDesigner; + +})(typeof window !== 'undefined' ? window : this);