| 1 |
( function () { |
| 2 |
var el = React.createElement; |
| 3 |
var __ = wp.i18n.__; |
| 4 |
var registerBlockType = wp.blocks.registerBlockType; |
| 5 |
var useBlockProps = wp.blockEditor.useBlockProps; |
| 6 |
var InspectorControls = wp.blockEditor.InspectorControls; |
| 7 |
var MediaUpload = wp.blockEditor.MediaUpload; |
| 8 |
var MediaUploadCheck = wp.blockEditor.MediaUploadCheck; |
| 9 |
var PanelBody = wp.components.PanelBody; |
| 10 |
var PanelRow = wp.components.PanelRow; |
| 11 |
var ToggleControl = wp.components.ToggleControl; |
| 12 |
var RangeControl = wp.components.RangeControl; |
| 13 |
var SelectControl = wp.components.SelectControl; |
| 14 |
var Button = wp.components.Button; |
| 15 |
var PanelColorSettings = wp.blockEditor.PanelColorSettings; |
| 16 |
|
| 17 |
function getTypographyControl() { |
| 18 |
return (window.bkbgTypographyControl || function () { return null; }); |
| 19 |
} |
| 20 |
|
| 21 |
// ── Editor-side k-means palette extractor (simplified) ────────── |
| 22 |
function rgbToHex(r, g, b) { |
| 23 |
return '#' + [r, g, b].map(function (v) { |
| 24 |
return ('0' + Math.round(v).toString(16)).slice(-2); |
| 25 |
}).join(''); |
| 26 |
} |
| 27 |
|
| 28 |
function hexToRgb(hex) { |
| 29 |
var r = parseInt(hex.slice(1, 3), 16); |
| 30 |
var g = parseInt(hex.slice(3, 5), 16); |
| 31 |
var b = parseInt(hex.slice(5, 7), 16); |
| 32 |
return [r, g, b]; |
| 33 |
} |
| 34 |
|
| 35 |
function rgbToHsl(r, g, b) { |
| 36 |
r /= 255; g /= 255; b /= 255; |
| 37 |
var max = Math.max(r, g, b), min = Math.min(r, g, b); |
| 38 |
var h, s, l = (max + min) / 2; |
| 39 |
if (max === min) { h = s = 0; } |
| 40 |
else { |
| 41 |
var d = max - min; |
| 42 |
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); |
| 43 |
switch (max) { |
| 44 |
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; |
| 45 |
case g: h = ((b - r) / d + 2) / 6; break; |
| 46 |
default: h = ((r - g) / d + 4) / 6; break; |
| 47 |
} |
| 48 |
} |
| 49 |
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)]; |
| 50 |
} |
| 51 |
|
| 52 |
// Sample pixels and run k-means quantization on canvas |
| 53 |
function extractPalette(imgEl, numColors, quality) { |
| 54 |
var canvas = document.createElement('canvas'); |
| 55 |
var scale = Math.min(1, 100 / Math.max(imgEl.naturalWidth || imgEl.width, imgEl.naturalHeight || imgEl.height)); |
| 56 |
canvas.width = Math.max(1, Math.floor((imgEl.naturalWidth || imgEl.width) * scale)); |
| 57 |
canvas.height = Math.max(1, Math.floor((imgEl.naturalHeight || imgEl.height) * scale)); |
| 58 |
var ctx = canvas.getContext('2d'); |
| 59 |
ctx.drawImage(imgEl, 0, 0, canvas.width, canvas.height); |
| 60 |
var data; |
| 61 |
try { data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; } |
| 62 |
catch (e) { return null; } |
| 63 |
|
| 64 |
// Collect sample pixels |
| 65 |
var pixels = []; |
| 66 |
for (var i = 0; i < data.length; i += 4 * quality) { |
| 67 |
var a = data[i + 3]; |
| 68 |
if (a < 128) continue; |
| 69 |
pixels.push([data[i], data[i + 1], data[i + 2]]); |
| 70 |
} |
| 71 |
if (pixels.length === 0) return null; |
| 72 |
|
| 73 |
// Simple k-means (5 iterations) |
| 74 |
var k = Math.min(numColors, pixels.length); |
| 75 |
var centroids = pixels.slice(0, k).map(function (p) { return p.slice(); }); |
| 76 |
for (var iter = 0; iter < 5; iter++) { |
| 77 |
var clusters = centroids.map(function () { return []; }); |
| 78 |
pixels.forEach(function (p) { |
| 79 |
var best = 0, bestD = Infinity; |
| 80 |
centroids.forEach(function (c, ci) { |
| 81 |
var d = Math.pow(p[0]-c[0],2)+Math.pow(p[1]-c[1],2)+Math.pow(p[2]-c[2],2); |
| 82 |
if (d < bestD) { bestD = d; best = ci; } |
| 83 |
}); |
| 84 |
clusters[best].push(p); |
| 85 |
}); |
| 86 |
clusters.forEach(function (cl, ci) { |
| 87 |
if (cl.length === 0) return; |
| 88 |
centroids[ci] = [ |
| 89 |
cl.reduce(function (s, p) { return s + p[0]; }, 0) / cl.length, |
| 90 |
cl.reduce(function (s, p) { return s + p[1]; }, 0) / cl.length, |
| 91 |
cl.reduce(function (s, p) { return s + p[2]; }, 0) / cl.length |
| 92 |
]; |
| 93 |
}); |
| 94 |
} |
| 95 |
return centroids.map(function (c) { |
| 96 |
return { r: Math.round(c[0]), g: Math.round(c[1]), b: Math.round(c[2]) }; |
| 97 |
}); |
| 98 |
} |
| 99 |
|
| 100 |
// ── Sample swatches for editor preview when no image ──────────── |
| 101 |
var SAMPLE_SWATCHES = ['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c']; |
| 102 |
|
| 103 |
// ── Swatch grid component ──────────────────────────────────────── |
| 104 |
function SwatchGrid(props) { |
| 105 |
var attr = props.attr; |
| 106 |
var swatches = props.swatches || SAMPLE_SWATCHES; |
| 107 |
var size = attr.swatchSize; |
| 108 |
var radius = attr.swatchRadius; |
| 109 |
var gap = attr.swatchGap; |
| 110 |
|
| 111 |
var gridStyle = { display: 'flex', flexWrap: 'wrap', gap: gap + 'px', justifyContent: 'center', padding: '12px 0' }; |
| 112 |
if (attr.layout === 'grid') { |
| 113 |
gridStyle = { display: 'grid', gridTemplateColumns: 'repeat(' + attr.swatchColumns + ', 1fr)', gap: gap + 'px', padding: '12px 0' }; |
| 114 |
} else if (attr.layout === 'list') { |
| 115 |
gridStyle = { display: 'flex', flexDirection: 'column', gap: gap + 'px', padding: '12px 0' }; |
| 116 |
} |
| 117 |
|
| 118 |
return el('div', { style: gridStyle }, |
| 119 |
swatches.map(function (hex, i) { |
| 120 |
var rgb = hexToRgb(hex.length === 7 ? hex : rgbToHex(hex.r || 0, hex.g || 0, hex.b || 0)); |
| 121 |
var hsl = rgbToHsl(rgb[0], rgb[1], rgb[2]); |
| 122 |
var hexVal = hex.length === 7 ? hex : rgbToHex(rgb[0], rgb[1], rgb[2]); |
| 123 |
var label = attr.showHexValues ? hexVal : |
| 124 |
attr.showRgbValues ? 'rgb(' + rgb.join(',') + ')' : |
| 125 |
attr.showHslValues ? 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)' : ''; |
| 126 |
|
| 127 |
return el('div', { key: i, className: 'bkbg-ct-swatch', style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px' } }, |
| 128 |
el('div', { style: { width: size + 'px', height: size + 'px', background: hexVal, borderRadius: radius + 'px', border: '1px solid rgba(0,0,0,.1)', boxShadow: '0 2px 8px rgba(0,0,0,.15)', cursor: attr.showCopyButton ? 'pointer' : 'default' }, title: attr.showCopyButton ? 'Copy: ' + hexVal : hexVal }), |
| 129 |
(attr.showHexValues || attr.showRgbValues || attr.showHslValues) && |
| 130 |
el('span', { className: 'bkbg-ct-swatch-label', style: { color: attr.labelColor, maxWidth: size + 'px' } }, label) |
| 131 |
); |
| 132 |
}) |
| 133 |
); |
| 134 |
} |
| 135 |
|
| 136 |
// ── Register block ─────────────────────────────────────────────── |
| 137 |
registerBlockType('blockenberg/color-thief', { |
| 138 |
title: 'Color Thief', |
| 139 |
icon: 'art', |
| 140 |
category: 'bkbg-dev', |
| 141 |
|
| 142 |
edit: function (props) { |
| 143 |
var attr = props.attributes; |
| 144 |
var setAttr = props.setAttributes; |
| 145 |
|
| 146 |
var imgRef = React.useRef(null); |
| 147 |
var _state = React.useState(null); |
| 148 |
var swatches = _state[0]; var setSwatches = _state[1]; |
| 149 |
|
| 150 |
// Extract when imageUrl changes |
| 151 |
React.useEffect(function () { |
| 152 |
if (!attr.imageUrl) { setSwatches(null); return; } |
| 153 |
var img = new window.Image(); |
| 154 |
img.crossOrigin = 'anonymous'; |
| 155 |
img.onload = function () { |
| 156 |
var palette = extractPalette(img, attr.paletteSize, attr.quality); |
| 157 |
if (palette) { |
| 158 |
setSwatches(palette.map(function (c) { return rgbToHex(c.r, c.g, c.b); })); |
| 159 |
} |
| 160 |
}; |
| 161 |
img.onerror = function () { setSwatches(null); }; |
| 162 |
img.src = attr.imageUrl; |
| 163 |
}, [attr.imageUrl, attr.paletteSize, attr.quality]); |
| 164 |
|
| 165 |
var displaySwatches = swatches || SAMPLE_SWATCHES; |
| 166 |
|
| 167 |
return el(React.Fragment, null, |
| 168 |
el(InspectorControls, null, |
| 169 |
// Image |
| 170 |
el(PanelBody, { title: 'Image', initialOpen: true }, |
| 171 |
el(MediaUploadCheck, null, |
| 172 |
el(MediaUpload, { |
| 173 |
onSelect: function (media) { |
| 174 |
setAttr({ imageUrl: media.url, imageId: media.id, imageAlt: media.alt || '' }); |
| 175 |
}, |
| 176 |
allowedTypes: ['image'], |
| 177 |
value: attr.imageId, |
| 178 |
render: function (r) { |
| 179 |
return el('div', null, |
| 180 |
attr.imageUrl |
| 181 |
? el(Button, { variant: 'secondary', isSmall: true, onClick: r.open }, 'Change Image') |
| 182 |
: el(Button, { variant: 'primary', onClick: r.open }, 'Select Image'), |
| 183 |
attr.imageUrl && el(Button, { variant: 'tertiary', isSmall: true, isDestructive: true, style: { marginLeft: 8 }, onClick: function () { setAttr({ imageUrl: '', imageId: 0, imageAlt: '' }); setSwatches(null); } }, 'Remove') |
| 184 |
); |
| 185 |
} |
| 186 |
}) |
| 187 |
), |
| 188 |
el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show Image Preview', checked: attr.showImagePreview, onChange: function (v) { setAttr({ showImagePreview: v }); } }), |
| 189 |
attr.showImagePreview && el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Preview Height (px)', value: attr.imageHeight, min: 80, max: 600, onChange: function (v) { setAttr({ imageHeight: v }); } }), |
| 190 |
attr.showImagePreview && el(SelectControl, { __nextHasNoMarginBottom: true, label: 'Object Fit', value: attr.imageObjectFit, options: [{value:'cover',label:'Cover'},{value:'contain',label:'Contain'},{value:'fill',label:'Fill'}], onChange: function (v) { setAttr({ imageObjectFit: v }); } }) |
| 191 |
), |
| 192 |
// Palette |
| 193 |
el(PanelBody, { title: 'Palette Settings', initialOpen: true }, |
| 194 |
el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Number of Colors', value: attr.paletteSize, min: 2, max: 12, onChange: function (v) { setAttr({ paletteSize: v }); } }), |
| 195 |
el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Sampling Quality', help: 'Lower = more accurate, slower', value: attr.quality, min: 1, max: 20, onChange: function (v) { setAttr({ quality: v }); } }), |
| 196 |
el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show Dominant Color Banner', checked: attr.showDominant, onChange: function (v) { setAttr({ showDominant: v }); } }), |
| 197 |
el(SelectControl, { __nextHasNoMarginBottom: true, label: 'Layout', value: attr.layout, options: [{value:'row',label:'Row (flex)'},{value:'grid',label:'Grid'},{value:'list',label:'List'}], onChange: function (v) { setAttr({ layout: v }); } }), |
| 198 |
attr.layout === 'grid' && el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Grid Columns', value: attr.swatchColumns, min: 2, max: 12, onChange: function (v) { setAttr({ swatchColumns: v }); } }) |
| 199 |
), |
| 200 |
// Swatches |
| 201 |
el(PanelBody, { title: 'Swatch Appearance', initialOpen: false }, |
| 202 |
el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Swatch Size (px)', value: attr.swatchSize, min: 24, max: 150, onChange: function (v) { setAttr({ swatchSize: v }); } }), |
| 203 |
el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Corner Radius (px)', value: attr.swatchRadius, min: 0, max: 60, onChange: function (v) { setAttr({ swatchRadius: v }); } }), |
| 204 |
el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Gap (px)', value: attr.swatchGap, min: 2, max: 40, onChange: function (v) { setAttr({ swatchGap: v }); } }), |
| 205 |
el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show Copy Button', checked: attr.showCopyButton, onChange: function (v) { setAttr({ showCopyButton: v }); } }), |
| 206 |
attr.showCopyButton && el(SelectControl, { __nextHasNoMarginBottom: true, label: 'Copy Format', value: attr.copyFormat, options: [{value:'hex',label:'HEX (#rrggbb)'},{value:'rgb',label:'RGB'},{value:'hsl',label:'HSL'}], onChange: function (v) { setAttr({ copyFormat: v }); } }) |
| 207 |
), |
| 208 |
// Labels |
| 209 |
el(PanelBody, { title: 'Color Labels', initialOpen: false }, |
| 210 |
el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show HEX Values', checked: attr.showHexValues, onChange: function (v) { setAttr({ showHexValues: v }); } }), |
| 211 |
el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show RGB Values', checked: attr.showRgbValues, onChange: function (v) { setAttr({ showRgbValues: v }); } }), |
| 212 |
el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show HSL Values', checked: attr.showHslValues, onChange: function (v) { setAttr({ showHslValues: v }); } }) |
| 213 |
), |
| 214 |
// Typography |
| 215 |
el(PanelBody, { title: 'Typography', initialOpen: false }, |
| 216 |
(function () { |
| 217 |
var TC = getTypographyControl(); |
| 218 |
return el(TC, { label: 'Label', value: attr.typoLabel || {}, onChange: function (v) { setAttr({ typoLabel: v }); } }); |
| 219 |
})() |
| 220 |
), |
| 221 |
// Style |
| 222 |
el(PanelBody, { title: 'Style', initialOpen: false }, |
| 223 |
el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Block Border Radius (px)', value: attr.borderRadius, min: 0, max: 40, onChange: function (v) { setAttr({ borderRadius: v }); } }), |
| 224 |
el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Padding (px)', value: attr.padding, min: 0, max: 60, onChange: function (v) { setAttr({ padding: v }); } }) |
| 225 |
), |
| 226 |
el(PanelColorSettings, { |
| 227 |
title: 'Colors', |
| 228 |
initialOpen: false, |
| 229 |
colorSettings: [ |
| 230 |
{ value: attr.bgColor, onChange: function (v) { setAttr({ bgColor: v || '#ffffff' }); }, label: 'Background' }, |
| 231 |
{ value: attr.labelColor, onChange: function (v) { setAttr({ labelColor: v || '#374151' }); }, label: 'Label Text' }, |
| 232 |
{ value: attr.borderColor, onChange: function (v) { setAttr({ borderColor: v || '#e5e7eb' }); }, label: 'Border' } |
| 233 |
] |
| 234 |
}) |
| 235 |
), |
| 236 |
|
| 237 |
// ── Editor preview ─────────────────────────────────── |
| 238 |
el('div', useBlockProps({ className: 'bkbg-ct-editor' }), |
| 239 |
el('div', { |
| 240 |
style: { |
| 241 |
backgroundColor: attr.bgColor, |
| 242 |
borderRadius: attr.borderRadius + 'px', |
| 243 |
border: '1px solid ' + attr.borderColor, |
| 244 |
padding: attr.padding + 'px', |
| 245 |
overflow: 'hidden' |
| 246 |
} |
| 247 |
}, |
| 248 |
attr.showImagePreview && el('div', { style: { marginBottom: '12px', borderRadius: (attr.borderRadius > 4 ? attr.borderRadius - 4 : 0) + 'px', overflow: 'hidden' } }, |
| 249 |
attr.imageUrl |
| 250 |
? el('img', { src: attr.imageUrl, alt: attr.imageAlt, style: { width: '100%', height: attr.imageHeight + 'px', objectFit: attr.imageObjectFit, display: 'block' } }) |
| 251 |
: el('div', { style: { width: '100%', height: attr.imageHeight + 'px', background: '#f3f4f6', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#9ca3af' } }, '📷 Select an image to extract its palette') |
| 252 |
), |
| 253 |
attr.showDominant && displaySwatches.length > 0 && |
| 254 |
el('div', { style: { height: '32px', background: displaySwatches[0], borderRadius: '6px', marginBottom: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center' } }, |
| 255 |
el('span', { style: { color: '#fff', fontSize: '11px', fontWeight: 700, textShadow: '0 1px 2px rgba(0,0,0,.5)' } }, 'DOMINANT') |
| 256 |
), |
| 257 |
!attr.imageUrl && el('div', { style: { textAlign: 'center', fontSize: '12px', color: '#9ca3af', marginBottom: '8px' } }, 'Sample palette — upload an image to see real colors'), |
| 258 |
el(SwatchGrid, { attr: attr, swatches: displaySwatches }) |
| 259 |
) |
| 260 |
) |
| 261 |
); |
| 262 |
}, |
| 263 |
|
| 264 |
save: function (props) { |
| 265 |
var attr = props.attributes; |
| 266 |
return el('div', useBlockProps.save(), |
| 267 |
el('div', { |
| 268 |
className: 'bkbg-ct-app', |
| 269 |
'data-opts': JSON.stringify(attr) |
| 270 |
}) |
| 271 |
); |
| 272 |
} |
| 273 |
}); |
| 274 |
}() ); |
| 275 |
|