| 1 |
( function () { |
| 2 |
var el = wp.element.createElement; |
| 3 |
var __ = wp.i18n.__; |
| 4 |
var registerBlockType = wp.blocks.registerBlockType; |
| 5 |
var InspectorControls = wp.blockEditor.InspectorControls; |
| 6 |
var useBlockProps = wp.blockEditor.useBlockProps; |
| 7 |
var RichText = wp.blockEditor.RichText; |
| 8 |
var PanelColorSettings = wp.blockEditor.PanelColorSettings; |
| 9 |
var PanelBody = wp.components.PanelBody; |
| 10 |
var ToggleControl = wp.components.ToggleControl; |
| 11 |
var RangeControl = wp.components.RangeControl; |
| 12 |
var SelectControl = wp.components.SelectControl; |
| 13 |
var TextControl = wp.components.TextControl; |
| 14 |
var Button = wp.components.Button; |
| 15 |
var Fragment = wp.element.Fragment; |
| 16 |
|
| 17 |
var _flTC, _flTV; |
| 18 |
function _tc() { return _flTC || (_flTC = window.bkbgTypographyControl); } |
| 19 |
function _tv() { return _flTV || (_flTV = window.bkbgTypoCssVars); } |
| 20 |
|
| 21 |
/* Status icon sets */ |
| 22 |
var STATUS_ICONS = { |
| 23 |
'color-check': { included: '✓', 'not-included': '✗', partial: '◐', 'coming-soon': '⏱' }, |
| 24 |
'emoji': { included: '� |
| 25 |
', 'not-included': '❌', partial: '🔶', 'coming-soon': '🔜' }, |
| 26 |
'minimal': { included: '•', 'not-included': '–', partial: '~', 'coming-soon': '…' } |
| 27 |
}; |
| 28 |
|
| 29 |
function getStatusColor(status, a) { |
| 30 |
if (status === 'included') return a.includedColor; |
| 31 |
if (status === 'not-included') return a.notIncludedColor; |
| 32 |
if (status === 'partial') return a.partialColor; |
| 33 |
if (status === 'coming-soon') return a.comingSoonColor; |
| 34 |
return a.labelColor; |
| 35 |
} |
| 36 |
|
| 37 |
function getStatusIcon(status, iconSet) { |
| 38 |
var icons = STATUS_ICONS[iconSet] || STATUS_ICONS['color-check']; |
| 39 |
return icons[status] || '•'; |
| 40 |
} |
| 41 |
|
| 42 |
registerBlockType('blockenberg/feature-list', { |
| 43 |
edit: function (props) { |
| 44 |
var attr = props.attributes; |
| 45 |
var set = props.setAttributes; |
| 46 |
var items = attr.items || []; |
| 47 |
|
| 48 |
function updateItem(idx, field, val) { |
| 49 |
var updated = items.map(function (it, i) { |
| 50 |
if (i !== idx) return it; |
| 51 |
var p = {}; p[field] = val; |
| 52 |
return Object.assign({}, it, p); |
| 53 |
}); |
| 54 |
set({ items: updated }); |
| 55 |
} |
| 56 |
|
| 57 |
function addItem() { |
| 58 |
set({ items: items.concat([{ label: 'New Feature', status: 'included', description: '', badge: '', group: '' }]) }); |
| 59 |
} |
| 60 |
|
| 61 |
function removeItem(idx) { set({ items: items.filter(function (_, i) { return i !== idx; }) }); } |
| 62 |
|
| 63 |
function moveItem(idx, dir) { |
| 64 |
var arr = items.slice(); |
| 65 |
var to = idx + dir; |
| 66 |
if (to < 0 || to >= arr.length) return; |
| 67 |
var tmp = arr[idx]; arr[idx] = arr[to]; arr[to] = tmp; |
| 68 |
set({ items: arr }); |
| 69 |
} |
| 70 |
|
| 71 |
var inspector = el(InspectorControls, {}, |
| 72 |
el(PanelBody, { title: __('Style', 'blockenberg'), initialOpen: true }, |
| 73 |
el(SelectControl, { |
| 74 |
label: __('List Style', 'blockenberg'), value: attr.listStyle, |
| 75 |
options: [ |
| 76 |
{ label: 'Bordered rows', value: 'bordered' }, |
| 77 |
{ label: 'Striped rows', value: 'striped' }, |
| 78 |
{ label: 'Cards', value: 'card' }, |
| 79 |
{ label: 'Compact (no lines)', value: 'compact' } |
| 80 |
], |
| 81 |
onChange: function (v) { set({ listStyle: v }); }, __nextHasNoMarginBottom: true |
| 82 |
}), |
| 83 |
el(SelectControl, { |
| 84 |
label: __('Icon Set', 'blockenberg'), value: attr.iconSet, |
| 85 |
options: [ |
| 86 |
{ label: 'Color check / cross', value: 'color-check' }, |
| 87 |
{ label: 'Emoji', value: 'emoji' }, |
| 88 |
{ label: 'Minimal (dot / dash)', value: 'minimal' } |
| 89 |
], |
| 90 |
onChange: function (v) { set({ iconSet: v }); }, __nextHasNoMarginBottom: true |
| 91 |
}), |
| 92 |
el(RangeControl, { label: __('Icon Size (px)', 'blockenberg'), value: attr.iconSize, min: 14, max: 32, onChange: function (v) { set({ iconSize: v }); }, __nextHasNoMarginBottom: true }), |
| 93 |
el(RangeControl, { label: __('Border Radius (px)', 'blockenberg'), value: attr.borderRadius, min: 0, max: 24, onChange: function (v) { set({ borderRadius: v }); }, __nextHasNoMarginBottom: true }) |
| 94 |
), |
| 95 |
el(PanelBody, { title: __('Visibility', 'blockenberg'), initialOpen: false }, |
| 96 |
el(ToggleControl, { label: __('Show Heading', 'blockenberg'), checked: attr.showHeading, onChange: function (v) { set({ showHeading: v }); }, __nextHasNoMarginBottom: true }), |
| 97 |
el(ToggleControl, { label: __('Show Descriptions', 'blockenberg'), checked: attr.showDesc, onChange: function (v) { set({ showDesc: v }); }, __nextHasNoMarginBottom: true }), |
| 98 |
el(ToggleControl, { label: __('Show Badges', 'blockenberg'), checked: attr.showBadge, onChange: function (v) { set({ showBadge: v }); }, __nextHasNoMarginBottom: true }), |
| 99 |
el(ToggleControl, { label: __('Show "Not Included" Items', 'blockenberg'), checked: attr.showNotIncluded, onChange: function (v) { set({ showNotIncluded: v }); }, __nextHasNoMarginBottom: true }), |
| 100 |
el(ToggleControl, { label: __('Strike Through "Not Included"', 'blockenberg'), checked: attr.strikeNotIncluded, onChange: function (v) { set({ strikeNotIncluded: v }); }, __nextHasNoMarginBottom: true }) |
| 101 |
), |
| 102 |
el(PanelBody, { title: __('Items', 'blockenberg'), initialOpen: true }, |
| 103 |
items.map(function (it, idx) { |
| 104 |
return el('div', { key: idx, style: { border: '1px solid #e5e7eb', borderRadius: '8px', padding: '10px', marginBottom: '10px' } }, |
| 105 |
el('div', { style: { display: 'flex', gap: '6px', alignItems: 'center', marginBottom: '8px' } }, |
| 106 |
el('span', { style: { fontWeight: '700', fontSize: '12px', color: getStatusColor(it.status, attr), minWidth: '16px' } }, getStatusIcon(it.status, attr.iconSet)), |
| 107 |
el(SelectControl, { |
| 108 |
value: it.status, |
| 109 |
options: [ |
| 110 |
{ label: '✓ Included', value: 'included' }, |
| 111 |
{ label: '✗ Not included', value: 'not-included' }, |
| 112 |
{ label: '◐ Partial', value: 'partial' }, |
| 113 |
{ label: '⏱ Coming soon', value: 'coming-soon' } |
| 114 |
], |
| 115 |
onChange: function (v) { updateItem(idx, 'status', v); }, |
| 116 |
style: { flex: 1, margin: 0 }, |
| 117 |
__nextHasNoMarginBottom: true |
| 118 |
}), |
| 119 |
el(Button, { onClick: function () { moveItem(idx, -1); }, isSmall: true, variant: 'tertiary', disabled: idx === 0 }, '↑'), |
| 120 |
el(Button, { onClick: function () { moveItem(idx, 1); }, isSmall: true, variant: 'tertiary', disabled: idx === items.length - 1 }, '↓'), |
| 121 |
el(Button, { onClick: function () { removeItem(idx); }, isSmall: true, variant: 'link', isDestructive: true }, '✕') |
| 122 |
), |
| 123 |
el(TextControl, { label: __('Label', 'blockenberg'), value: it.label, onChange: function (v) { updateItem(idx, 'label', v); }, __nextHasNoMarginBottom: true }), |
| 124 |
el(TextControl, { label: __('Description (optional)', 'blockenberg'), value: it.description, onChange: function (v) { updateItem(idx, 'description', v); }, __nextHasNoMarginBottom: true }), |
| 125 |
el(TextControl, { label: __('Badge (optional)', 'blockenberg'), value: it.badge, onChange: function (v) { updateItem(idx, 'badge', v); }, __nextHasNoMarginBottom: true }), |
| 126 |
el(TextControl, { label: __('Group Header (leave blank to skip)', 'blockenberg'), value: it.group, onChange: function (v) { updateItem(idx, 'group', v); }, __nextHasNoMarginBottom: true }) |
| 127 |
); |
| 128 |
}), |
| 129 |
el(Button, { onClick: addItem, variant: 'primary', isSmall: true }, __('+ Add Feature', 'blockenberg')) |
| 130 |
), |
| 131 |
el(PanelBody, { title: __('Spacing', 'blockenberg'), initialOpen: false }, |
| 132 |
el(RangeControl, { label: __('Padding Top (px)', 'blockenberg'), value: attr.paddingTop, min: 0, max: 240, onChange: function (v) { set({ paddingTop: v }); }, __nextHasNoMarginBottom: true }), |
| 133 |
el(RangeControl, { label: __('Padding Bottom (px)', 'blockenberg'), value: attr.paddingBottom, min: 0, max: 240, onChange: function (v) { set({ paddingBottom: v }); }, __nextHasNoMarginBottom: true }) |
| 134 |
), |
| 135 |
|
| 136 |
el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false }, |
| 137 |
_tc() && el(_tc(), { |
| 138 |
label: __('Heading Typography', 'blockenberg'), |
| 139 |
value: attr.typoHeading, |
| 140 |
onChange: function (v) { set({ typoHeading: v }); } |
| 141 |
}), |
| 142 |
_tc() && el(_tc(), { |
| 143 |
label: __('Label Typography', 'blockenberg'), |
| 144 |
value: attr.typoLabel, |
| 145 |
onChange: function (v) { set({ typoLabel: v }); } |
| 146 |
}), |
| 147 |
_tc() && el(_tc(), { |
| 148 |
label: __('Description Typography', 'blockenberg'), |
| 149 |
value: attr.typoDesc, |
| 150 |
onChange: function (v) { set({ typoDesc: v }); } |
| 151 |
}) |
| 152 |
), |
| 153 |
el(PanelColorSettings, { |
| 154 |
title: __('Colors', 'blockenberg'), initialOpen: false, |
| 155 |
colorSettings: [ |
| 156 |
{ value: attr.bgColor, onChange: function (v) { set({ bgColor: v || '' }); }, label: __('Section BG', 'blockenberg') }, |
| 157 |
{ value: attr.itemBg, onChange: function (v) { set({ itemBg: v || '#ffffff' }); }, label: __('Row BG', 'blockenberg') }, |
| 158 |
{ value: attr.stripeBg, onChange: function (v) { set({ stripeBg: v || '#f9fafb' }); }, label: __('Stripe BG', 'blockenberg') }, |
| 159 |
{ value: attr.borderColor, onChange: function (v) { set({ borderColor: v || '#e5e7eb' }); }, label: __('Border', 'blockenberg') }, |
| 160 |
{ value: attr.labelColor, onChange: function (v) { set({ labelColor: v || '#111827' }); }, label: __('Label', 'blockenberg') }, |
| 161 |
{ value: attr.descColor, onChange: function (v) { set({ descColor: v || '#6b7280' }); }, label: __('Description', 'blockenberg') }, |
| 162 |
{ value: attr.headingColor, onChange: function (v) { set({ headingColor: v || '#111827' }); }, label: __('Heading', 'blockenberg') }, |
| 163 |
{ value: attr.includedColor, onChange: function (v) { set({ includedColor: v || '#10b981' }); }, label: __('Included Icon', 'blockenberg') }, |
| 164 |
{ value: attr.notIncludedColor, onChange: function (v) { set({ notIncludedColor: v || '#d1d5db' }); }, label: __('Not Included Icon', 'blockenberg') }, |
| 165 |
{ value: attr.partialColor, onChange: function (v) { set({ partialColor: v || '#f59e0b' }); }, label: __('Partial Icon', 'blockenberg') }, |
| 166 |
{ value: attr.comingSoonColor, onChange: function (v) { set({ comingSoonColor: v || '#8b5cf6' }); }, label: __('Coming Soon Icon', 'blockenberg') }, |
| 167 |
{ value: attr.badgeBg, onChange: function (v) { set({ badgeBg: v || '#ede9fe' }); }, label: __('Badge BG', 'blockenberg') }, |
| 168 |
{ value: attr.badgeColor, onChange: function (v) { set({ badgeColor: v || '#7c3aed' }); }, label: __('Badge Text', 'blockenberg') } |
| 169 |
] |
| 170 |
}) |
| 171 |
); |
| 172 |
|
| 173 |
/* Editor preview */ |
| 174 |
var wrapStyle = Object.assign({ |
| 175 |
background: attr.bgColor || 'transparent', |
| 176 |
paddingTop: attr.paddingTop + 'px', |
| 177 |
paddingBottom: attr.paddingBottom + 'px' |
| 178 |
}, _tv()(attr.typoHeading, '--bkbg-fl-hd-'), _tv()(attr.typoLabel, '--bkbg-fl-lb-'), _tv()(attr.typoDesc, '--bkbg-fl-ds-')); |
| 179 |
|
| 180 |
var listWrapStyle = { |
| 181 |
borderRadius: attr.listStyle === 'card' ? attr.borderRadius + 'px' : 0, |
| 182 |
border: attr.listStyle === 'bordered' || attr.listStyle === 'card' ? ('1px solid ' + attr.borderColor) : 'none', |
| 183 |
overflow: 'hidden' |
| 184 |
}; |
| 185 |
|
| 186 |
var prevGroup = ''; |
| 187 |
var rowEls = []; |
| 188 |
items.forEach(function (it, idx) { |
| 189 |
if (!attr.showNotIncluded && it.status === 'not-included') return; |
| 190 |
|
| 191 |
/* Group header */ |
| 192 |
if (it.group && it.group !== prevGroup) { |
| 193 |
rowEls.push(el('div', { key: 'g-' + idx, style: { padding: '8px 16px 4px', fontSize: '11px', fontWeight: '700', textTransform: 'uppercase', letterSpacing: '0.08em', color: attr.descColor, background: attr.stripeBg, borderBottom: '1px solid ' + attr.borderColor } }, it.group)); |
| 194 |
prevGroup = it.group; |
| 195 |
} |
| 196 |
|
| 197 |
var isBordered = attr.listStyle === 'bordered' || attr.listStyle === 'card'; |
| 198 |
var isStriped = attr.listStyle === 'striped'; |
| 199 |
var rowBg = isStriped && idx % 2 === 1 ? attr.stripeBg : attr.itemBg; |
| 200 |
var iconColor = getStatusColor(it.status, attr); |
| 201 |
var icon = getStatusIcon(it.status, attr.iconSet); |
| 202 |
var isNotIncl = it.status === 'not-included'; |
| 203 |
|
| 204 |
rowEls.push(el('div', { |
| 205 |
key: idx, |
| 206 |
style: { |
| 207 |
display: 'flex', alignItems: 'center', gap: '12px', padding: '12px 16px', |
| 208 |
background: rowBg, borderBottom: isBordered && idx < items.length - 1 ? ('1px solid ' + attr.borderColor) : 'none', |
| 209 |
marginBottom: attr.listStyle === 'card' ? '0' : (attr.gap > 0 ? attr.gap + 'px' : '0') |
| 210 |
} |
| 211 |
}, |
| 212 |
el('span', { style: { color: iconColor, fontSize: attr.iconSize + 'px', flexShrink: 0, width: attr.iconSize + 'px', textAlign: 'center', fontWeight: '700' } }, icon), |
| 213 |
el('div', { style: { flex: 1 } }, |
| 214 |
el('div', { className: 'bkbg-fl-label', style: { color: isNotIncl ? attr.notIncludedColor : attr.labelColor, textDecoration: isNotIncl && attr.strikeNotIncluded ? 'line-through' : 'none' } }, it.label), |
| 215 |
attr.showDesc && it.description && el('div', { className: 'bkbg-fl-desc', style: { color: attr.descColor } }, it.description) |
| 216 |
), |
| 217 |
attr.showBadge && it.badge && el('span', { style: { background: attr.badgeBg, color: attr.badgeColor, fontSize: '11px', fontWeight: '700', padding: '3px 8px', borderRadius: '20px', flexShrink: 0 } }, it.badge) |
| 218 |
)); |
| 219 |
}); |
| 220 |
|
| 221 |
return el(Fragment, {}, |
| 222 |
inspector, |
| 223 |
el('div', useBlockProps({ className: 'bkbg-fl-wrap', style: wrapStyle }), |
| 224 |
attr.showHeading && el(RichText, { |
| 225 |
tagName: 'h3', className: 'bkbg-fl-heading', value: attr.heading, |
| 226 |
onChange: function (v) { set({ heading: v }); }, |
| 227 |
placeholder: __('Heading…', 'blockenberg'), |
| 228 |
style: { color: attr.headingColor, marginBottom: '20px' } |
| 229 |
}), |
| 230 |
el('div', { style: listWrapStyle }, rowEls) |
| 231 |
) |
| 232 |
); |
| 233 |
}, |
| 234 |
|
| 235 |
save: function (props) { |
| 236 |
var el2 = wp.element.createElement; |
| 237 |
var ubp = wp.blockEditor.useBlockProps; |
| 238 |
return el2('div', ubp.save(), |
| 239 |
el2('div', { className: 'bkbg-fl-app', 'data-opts': JSON.stringify(props.attributes) }) |
| 240 |
); |
| 241 |
} |
| 242 |
}); |
| 243 |
}() ); |
| 244 |
|