# blockenberg/2.0.7/blocks/progress-bar/index.js

Blockenberg — 600+ Advanced Gutenberg Blocks &amp; AI Agent for WordPress Block Editor, version 2.0.7. 701 lines.

- Page: https://pluginprobe.com/plugins/blockenberg/2.0.7/code/blocks/progress-bar/index.js
- Raw: https://pluginprobe.com/plugins/blockenberg/2.0.7/raw/blocks/progress-bar/index.js
- Modified: 2026-03-06T12:32:40+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/blockenberg/2.0.7/code/blocks/progress-bar/index.js#L10-L20`.

```javascript
( function () {
    var el = wp.element.createElement;
    var Fragment = wp.element.Fragment;
    var __ = wp.i18n.__;
    var useState = wp.element.useState;
    var registerBlockType = wp.blocks.registerBlockType;
    var InspectorControls = wp.blockEditor.InspectorControls;
    var BlockControls = wp.blockEditor.BlockControls;
    var useBlockProps = wp.blockEditor.useBlockProps;
    var PanelBody = wp.components.PanelBody;
    var PanelColorSettings = wp.blockEditor.PanelColorSettings;
    var ToggleControl = wp.components.ToggleControl;
    var RangeControl = wp.components.RangeControl;
    var SelectControl = wp.components.SelectControl;
    var TextControl = wp.components.TextControl;
    var Button = wp.components.Button;
    var ColorPicker = wp.components.ColorPicker;
    var Popover = wp.components.Popover;
    var ToolbarGroup = wp.components.ToolbarGroup;
    var ToolbarButton = wp.components.ToolbarButton;

    var _tc; function getTypoControl() { return _tc || (_tc = window.bkbgTypographyControl); }
    var _tv; function getTypoCssVars() { return _tv || (_tv = window.bkbgTypoCssVars); }

    var v1Attributes = {
        items: { type: 'array', default: [{ label: 'Web Design', percentage: 90, color: '#3b82f6' }, { label: 'Development', percentage: 85, color: '#10b981' }, { label: 'Marketing', percentage: 75, color: '#f59e0b' }] },
        layoutStyle: { type: 'string', default: 'default' },
        showPercentage: { type: 'boolean', default: true },
        showLabels: { type: 'boolean', default: true },
        animateOnScroll: { type: 'boolean', default: true },
        animationDuration: { type: 'number', default: 1000 },
        barHeight: { type: 'number', default: 20 },
        barSpacing: { type: 'number', default: 24 },
        barRadius: { type: 'number', default: 10 },
        trackColor: { type: 'string', default: '#e5e7eb' },
        labelColor: { type: 'string', default: '#1f2937' },
        percentageColor: { type: 'string', default: '#6b7280' },
        labelSize: { type: 'number', default: 16 },
        percentageSize: { type: 'number', default: 14 },
        labelWeight: { type: 'number', default: 500 },
        percentagePosition: { type: 'string', default: 'right' },
        stripes: { type: 'boolean', default: false },
        stripesAnimated: { type: 'boolean', default: false },
        glowEffect: { type: 'boolean', default: false },
        gradient: { type: 'boolean', default: false },
        gradientAngle: { type: 'number', default: 90 },
        title: { type: 'string', default: '' },
        showTitle: { type: 'boolean', default: false },
        titleColor: { type: 'string', default: '#1f2937' },
        titleSize: { type: 'number', default: 24 },
        titleFontWeight: { type: 'number', default: 600 },
        titleLineHeight: { type: 'number', default: 1.3 },
        labelLineHeight: { type: 'number', default: 1.4 }
    };

    registerBlockType('blockenberg/progress-bar', {
        title: __('Progress Bar', 'blockenberg'),
        icon: 'chart-bar',
        category: 'bkbg-content',
        description: __('Display animated progress bars with customizable styles and labels.', 'blockenberg'),

        deprecated: [{
            attributes: v1Attributes,
            save: function (props) {
                var a = props.attributes;
                var wrapStyle = {
                    '--bkbg-pb-bar-height': a.barHeight + 'px',
                    '--bkbg-pb-bar-spacing': a.barSpacing + 'px',
                    '--bkbg-pb-bar-radius': a.barRadius + 'px',
                    '--bkbg-pb-track-color': a.trackColor,
                    '--bkbg-pb-label-color': a.labelColor,
                    '--bkbg-pb-label-size': a.labelSize + 'px',
                    '--bkbg-pb-label-weight': a.labelWeight,
                    '--bkbg-pb-label-line-height': a.labelLineHeight,
                    '--bkbg-pb-percentage-color': a.percentageColor,
                    '--bkbg-pb-percentage-size': a.percentageSize + 'px',
                    '--bkbg-pb-title-color': a.titleColor,
                    '--bkbg-pb-title-size': a.titleSize + 'px',
                    '--bkbg-pb-title-font-weight': a.titleFontWeight,
                    '--bkbg-pb-title-line-height': a.titleLineHeight,
                    '--bkbg-pb-animation-duration': a.animationDuration + 'ms'
                };
                var itemsData = a.items.map(function (item) {
                    return item.label + '|' + item.percentage + '|' + item.color;
                }).join(';;');
                var layoutClass = 'bkbg-pb-wrap';
                layoutClass += ' bkbg-pb-' + a.layoutStyle;
                if (a.stripes) layoutClass += ' bkbg-pb-striped';
                if (a.stripesAnimated) layoutClass += ' bkbg-pb-striped-animated';
                if (a.glowEffect) layoutClass += ' bkbg-pb-glow';
                if (a.gradient) layoutClass += ' bkbg-pb-gradient';
                var titleEl = null;
                if (a.showTitle && a.title) {
                    titleEl = el('div', { className: 'bkbg-pb-title' }, a.title);
                }
                return el('div', {
                    className: layoutClass,
                    style: wrapStyle,
                    'data-layout': a.layoutStyle,
                    'data-items': itemsData,
                    'data-animate': a.animateOnScroll ? '1' : '0',
                    'data-show-labels': a.showLabels ? '1' : '0',
                    'data-show-percentage': a.showPercentage ? '1' : '0',
                    'data-percentage-position': a.percentagePosition,
                    'data-gradient': a.gradient ? '1' : '0',
                    'data-gradient-angle': a.gradientAngle,
                    'data-glow': a.glowEffect ? '1' : '0'
                },
                    titleEl,
                    el('div', { className: 'bkbg-pb-list' })
                );
            }
        }],

        edit: function (props) {
            var attributes = props.attributes;
            var setAttributes = props.setAttributes;
            var isSelected = props.isSelected;
            var a = attributes;
            var TC = getTypoControl();

            // State for editing
            var editingState = useState(null);
            var editing = editingState[0];
            var setEditing = editingState[1];

            // State for hovered item
            var hoveredState = useState(null);
            var hovered = hoveredState[0];
            var setHovered = hoveredState[1];

            // State for color picker
            var colorPickerState = useState(null);
            var colorPicker = colorPickerState[0];
            var setColorPicker = colorPickerState[1];

            var layoutOptions = [
                { label: __('Default', 'blockenberg'), value: 'default' },
                { label: __('Rounded', 'blockenberg'), value: 'rounded' },
                { label: __('Flat', 'blockenberg'), value: 'flat' },
                { label: __('Thin', 'blockenberg'), value: 'thin' },
                { label: __('Thick', 'blockenberg'), value: 'thick' }
            ];

            var percentagePositionOptions = [
                { label: __('Right of Label', 'blockenberg'), value: 'right' },
                { label: __('Inside Bar', 'blockenberg'), value: 'inside' },
                { label: __('Above Bar', 'blockenberg'), value: 'above' },
                { label: __('Hidden', 'blockenberg'), value: 'hidden' }
            ];

            // Update item
            function updateItem(index, key, value) {
                var newItems = a.items.slice();
                newItems[index] = Object.assign({}, newItems[index]);
                newItems[index][key] = value;
                setAttributes({ items: newItems });
            }

            // Add item
            function addItem() {
                var colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'];
                var newColor = colors[a.items.length % colors.length];
                var newItems = a.items.concat([{
                    label: __('New Skill', 'blockenberg'),
                    percentage: 50,
                    color: newColor
                }]);
                setAttributes({ items: newItems });
            }

            // Remove item
            function removeItem(index) {
                var newItems = a.items.filter(function (_, i) { return i !== index; });
                setAttributes({ items: newItems });
            }

            // Move item
            function moveItem(index, direction) {
                var newIndex = index + direction;
                if (newIndex < 0 || newIndex >= a.items.length) return;
                var newItems = a.items.slice();
                var temp = newItems[index];
                newItems[index] = newItems[newIndex];
                newItems[newIndex] = temp;
                setAttributes({ items: newItems });
            }

            // Inspector controls
            var inspector = el(InspectorControls, {},
                el(PanelBody, { title: __('Layout', 'blockenberg'), initialOpen: true },
                    el(SelectControl, {
                        label: __('Style', 'blockenberg'),
                        value: a.layoutStyle,
                        options: layoutOptions,
                        onChange: function (v) { setAttributes({ layoutStyle: v }); }
                    }),
                    el(RangeControl, {
                        label: __('Bar Height', 'blockenberg'),
                        value: a.barHeight,
                        onChange: function (v) { setAttributes({ barHeight: v }); },
                        min: 4,
                        max: 60
                    }),
                    el(RangeControl, {
                        label: __('Bar Spacing', 'blockenberg'),
                        value: a.barSpacing,
                        onChange: function (v) { setAttributes({ barSpacing: v }); },
                        min: 8,
                        max: 60
                    }),
                    el(RangeControl, {
                        label: __('Border Radius', 'blockenberg'),
                        value: a.barRadius,
                        onChange: function (v) { setAttributes({ barRadius: v }); },
                        min: 0,
                        max: 30
                    })
                ),

                el(PanelBody, { title: __('Labels & Percentage', 'blockenberg'), initialOpen: false },
                    el(ToggleControl, {
                        label: __('Show Labels', 'blockenberg'),
                        checked: a.showLabels,
                        __nextHasNoMarginBottom: true,
                        onChange: function (v) { setAttributes({ showLabels: v }); }
                    }),
                    el(ToggleControl, {
                        label: __('Show Percentage', 'blockenberg'),
                        checked: a.showPercentage,
                        __nextHasNoMarginBottom: true,
                        onChange: function (v) { setAttributes({ showPercentage: v }); }
                    }),
                    a.showPercentage && el(SelectControl, {
                        label: __('Percentage Position', 'blockenberg'),
                        value: a.percentagePosition,
                        options: percentagePositionOptions,
                        onChange: function (v) { setAttributes({ percentagePosition: v }); }
                    }),
                ),

                el(PanelBody, { title: __('Effects', 'blockenberg'), initialOpen: false },
                    el(ToggleControl, {
                        label: __('Animate on Scroll', 'blockenberg'),
                        checked: a.animateOnScroll,
                        __nextHasNoMarginBottom: true,
                        onChange: function (v) { setAttributes({ animateOnScroll: v }); }
                    }),
                    a.animateOnScroll && el(RangeControl, {
                        label: __('Animation Duration (ms)', 'blockenberg'),
                        value: a.animationDuration,
                        onChange: function (v) { setAttributes({ animationDuration: v }); },
                        min: 200,
                        max: 3000,
                        step: 100
                    }),
                    el(ToggleControl, {
                        label: __('Stripes', 'blockenberg'),
                        checked: a.stripes,
                        __nextHasNoMarginBottom: true,
                        onChange: function (v) { setAttributes({ stripes: v }); }
                    }),
                    a.stripes && el(ToggleControl, {
                        label: __('Animated Stripes', 'blockenberg'),
                        checked: a.stripesAnimated,
                        __nextHasNoMarginBottom: true,
                        onChange: function (v) { setAttributes({ stripesAnimated: v }); }
                    }),
                    el(ToggleControl, {
                        label: __('Glow Effect', 'blockenberg'),
                        checked: a.glowEffect,
                        __nextHasNoMarginBottom: true,
                        onChange: function (v) { setAttributes({ glowEffect: v }); }
                    }),
                    el(ToggleControl, {
                        label: __('Gradient', 'blockenberg'),
                        checked: a.gradient,
                        __nextHasNoMarginBottom: true,
                        onChange: function (v) { setAttributes({ gradient: v }); }
                    }),
                    a.gradient && el(RangeControl, {
                        label: __('Gradient Angle', 'blockenberg'),
                        value: a.gradientAngle,
                        onChange: function (v) { setAttributes({ gradientAngle: v }); },
                        min: 0,
                        max: 360
                    })
                ),

                el(PanelBody, { title: __('Title', 'blockenberg'), initialOpen: false },
                    el(ToggleControl, {
                        label: __('Show Title', 'blockenberg'),
                        checked: a.showTitle,
                        __nextHasNoMarginBottom: true,
                        onChange: function (v) { setAttributes({ showTitle: v }); }
                    }),
                    ),

                el(PanelBody, { title: __('Progress Items', 'blockenberg'), initialOpen: false },
                    a.items.map(function (item, index) {
                        return el('div', { key: index, className: 'bkbg-pb-item-control' },
                            el('div', { className: 'bkbg-pb-item-header' },
                                el('span', { className: 'bkbg-pb-item-title' }, item.label || __('Item', 'blockenberg') + ' ' + (index + 1)),
                                el('div', { className: 'bkbg-pb-item-actions' },
                                    el(Button, {
                                        icon: 'arrow-up-alt2',
                                        size: 'small',
                                        disabled: index === 0,
                                        onClick: function () { moveItem(index, -1); }
                                    }),
                                    el(Button, {
                                        icon: 'arrow-down-alt2',
                                        size: 'small',
                                        disabled: index === a.items.length - 1,
                                        onClick: function () { moveItem(index, 1); }
                                    }),
                                    el(Button, {
                                        icon: 'trash',
                                        size: 'small',
                                        isDestructive: true,
                                        onClick: function () { removeItem(index); }
                                    })
                                )
                            ),
                            el(TextControl, {
                                label: __('Label', 'blockenberg'),
                                value: item.label,
                                onChange: function (v) { updateItem(index, 'label', v); }
                            }),
                            el(RangeControl, {
                                label: __('Percentage', 'blockenberg'),
                                value: item.percentage,
                                onChange: function (v) { updateItem(index, 'percentage', v); },
                                min: 0,
                                max: 100
                            }),
                            el('div', { className: 'bkbg-pb-color-row' },
                                el('span', {}, __('Color', 'blockenberg')),
                                el('button', {
                                    className: 'bkbg-pb-color-btn',
                                    style: { backgroundColor: item.color },
                                    onClick: function () { 
                                        setColorPicker(colorPicker === index ? null : index); 
                                    }
                                }),
                                colorPicker === index && el(Popover, {
                                    position: 'bottom left',
                                    onClose: function () { setColorPicker(null); }
                                },
                                    el(ColorPicker, {
                                        color: item.color,
                                        onChangeComplete: function (c) { 
                                            updateItem(index, 'color', c.hex); 
                                        }
                                    })
                                )
                            )
                        );
                    }),
                    el(Button, {
                        variant: 'secondary',
                        onClick: addItem,
                        className: 'bkbg-pb-add-btn'
                    }, __('+ Add Progress Bar', 'blockenberg'))
                ),

                
                el(PanelBody, { title: __('Typography', 'blockenberg'), initialOpen: false },
                    TC && a.showTitle && el(TC, { label: __('Title', 'blockenberg'), value: a.titleTypo || {}, onChange: function(v) { setAttributes({ titleTypo: v }); } }),
                    TC && a.showLabels && el(TC, { label: __('Label', 'blockenberg'), value: a.labelTypo || {}, onChange: function(v) { setAttributes({ labelTypo: v }); } }),
                    TC && a.showPercentage && el(TC, { label: __('Percentage', 'blockenberg'), value: a.pctTypo || {}, onChange: function(v) { setAttributes({ pctTypo: v }); } })
                ),
el(PanelColorSettings, {
                    title: __('Colors', 'blockenberg'),
                    initialOpen: false,
                    colorSettings: [
                        {
                            value: a.trackColor,
                            onChange: function (v) { setAttributes({ trackColor: v }); },
                            label: __('Track Color', 'blockenberg')
                        },
                        {
                            value: a.labelColor,
                            onChange: function (v) { setAttributes({ labelColor: v }); },
                            label: __('Label Color', 'blockenberg')
                        },
                        {
                            value: a.percentageColor,
                            onChange: function (v) { setAttributes({ percentageColor: v }); },
                            label: __('Percentage Color', 'blockenberg')
                        },
                        {
                            value: a.titleColor,
                            onChange: function (v) { setAttributes({ titleColor: v }); },
                            label: __('Title Color', 'blockenberg')
                        }
                    ]
                })
            );

            // Title element
            var titleEl = null;
            if (a.showTitle) {
                if (editing === 'title') {
                    titleEl = el('input', {
                        type: 'text',
                        className: 'bkbg-pb-title bkbg-pb-input-active',
                        value: a.title,
                        autoFocus: true,
                        placeholder: __('Progress Title', 'blockenberg'),
                        onChange: function (e) { setAttributes({ title: e.target.value }); },
                        onBlur: function () { setEditing(null); },
                        onKeyDown: function (e) { if (e.key === 'Enter') setEditing(null); }
                    });
                } else {
                    titleEl = el('div', {
                        className: 'bkbg-pb-title bkbg-pb-clickable',
                        onClick: function () { setEditing('title'); }
                    }, a.title || __('Progress Title', 'blockenberg'));
                }
            }

            // Lighten color helper
            function lightenColor(hex, percent) {
                var num = parseInt(hex.replace('#', ''), 16);
                var r = Math.min(255, (num >> 16) + Math.round(255 * percent / 100));
                var g = Math.min(255, ((num >> 8) & 0x00FF) + Math.round(255 * percent / 100));
                var b = Math.min(255, (num & 0x0000FF) + Math.round(255 * percent / 100));
                return '#' + (0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1);
            }

            // Get bar style
            function getBarStyle(item) {
                var style = {
                    width: item.percentage + '%'
                };

                // Background
                if (a.gradient) {
                    var lighterColor = lightenColor(item.color, 30);
                    style.background = 'linear-gradient(' + a.gradientAngle + 'deg, ' + item.color + ' 0%, ' + lighterColor + ' 50%, ' + item.color + ' 100%)';
                    style.backgroundSize = '200% 100%';
                } else {
                    style.backgroundColor = item.color;
                }

                // Stripes overlay
                if (a.stripes) {
                    var stripesBg = 'linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)';
                    if (a.gradient) {
                        var lighterColor = lightenColor(item.color, 30);
                        style.background = stripesBg + ', linear-gradient(' + a.gradientAngle + 'deg, ' + item.color + ' 0%, ' + lighterColor + ' 50%, ' + item.color + ' 100%)';
                    } else {
                        style.background = stripesBg + ', ' + item.color;
                    }
                    style.backgroundSize = '40px 40px, 100% 100%';
                }

                // Glow effect
                if (a.glowEffect) {
                    style.boxShadow = '0 0 10px ' + item.color + ', 0 0 20px ' + item.color + '60';
                }

                return style;
            }

            // Build progress bars
            var progressBars = a.items.map(function (item, index) {
                var labelKey = 'label-' + index;
                
                // Percentage element based on position
                var percentageEl = null;
                if (a.showPercentage && a.percentagePosition !== 'hidden') {
                    percentageEl = el('span', { className: 'bkbg-pb-percentage' }, item.percentage + '%');
                }

                // Label element - editable
                var labelEl = null;
                if (a.showLabels) {
                    if (editing === labelKey) {
                        labelEl = el('input', {
                            type: 'text',
                            className: 'bkbg-pb-label bkbg-pb-input-active',
                            value: item.label,
                            autoFocus: true,
                            onChange: function (e) { updateItem(index, 'label', e.target.value); },
                            onBlur: function () { setEditing(null); },
                            onKeyDown: function (e) { if (e.key === 'Enter') setEditing(null); }
                        });
                    } else {
                        labelEl = el('span', {
                            className: 'bkbg-pb-label bkbg-pb-clickable',
                            onClick: function () { setEditing(labelKey); }
                        }, item.label);
                    }
                }

                // Header with label and percentage (if position is right)
                var headerEl = null;
                if (a.showLabels || (a.showPercentage && a.percentagePosition === 'right')) {
                    headerEl = el('div', { className: 'bkbg-pb-header' },
                        labelEl,
                        a.percentagePosition === 'right' && percentageEl
                    );
                }

                // Above bar percentage
                var aboveEl = null;
                if (a.showPercentage && a.percentagePosition === 'above') {
                    aboveEl = el('div', { className: 'bkbg-pb-above' },
                        el('span', { 
                            className: 'bkbg-pb-percentage-above',
                            style: { left: item.percentage + '%' }
                        }, item.percentage + '%')
                    );
                }

                // Bar classes
                var barClasses = 'bkbg-pb-bar';
                if (a.stripes) barClasses += ' bkbg-pb-striped';
                if (a.stripesAnimated) barClasses += ' bkbg-pb-striped-animated';

                // Item actions (visible on hover)
                var itemActions = el('div', { className: 'bkbg-pb-item-actions' },
                    el(Button, {
                        icon: 'arrow-up-alt2',
                        size: 'small',
                        disabled: index === 0,
                        onClick: function (e) { e.stopPropagation(); moveItem(index, -1); },
                        label: __('Move Up', 'blockenberg')
                    }),
                    el(Button, {
                        icon: 'arrow-down-alt2',
                        size: 'small',
                        disabled: index === a.items.length - 1,
                        onClick: function (e) { e.stopPropagation(); moveItem(index, 1); },
                        label: __('Move Down', 'blockenberg')
                    }),
                    el(Button, {
                        icon: 'admin-page',
                        size: 'small',
                        onClick: function (e) { 
                            e.stopPropagation();
                            var newItems = a.items.slice();
                            newItems.splice(index + 1, 0, Object.assign({}, item));
                            setAttributes({ items: newItems });
                        },
                        label: __('Duplicate', 'blockenberg')
                    }),
                    el(Button, {
                        icon: 'trash',
                        size: 'small',
                        isDestructive: true,
                        onClick: function (e) { e.stopPropagation(); removeItem(index); },
                        label: __('Delete', 'blockenberg')
                    })
                );

                var itemClass = 'bkbg-pb-item';
                if (hovered === index) itemClass += ' bkbg-pb-item-hovered';

                return el('div', { 
                    className: itemClass, 
                    key: index,
                    onMouseEnter: function () { setHovered(index); },
                    onMouseLeave: function () { setHovered(null); }
                },
                    itemActions,
                    headerEl,
                    aboveEl,
                    el('div', { className: 'bkbg-pb-track' },
                        el('div', { 
                            className: barClasses,
                            style: getBarStyle(item)
                        },
                            a.percentagePosition === 'inside' && el('span', { className: 'bkbg-pb-percentage-inside' }, item.percentage + '%')
                        )
                    )
                );
            });

            // Layout classes
            var layoutClass = 'bkbg-pb-wrap bkbg-editor-wrap';
            layoutClass += ' bkbg-pb-' + a.layoutStyle;
            if (isSelected) layoutClass += ' bkbg-block-selected';

            // Block props for proper selection handling
            var blockProps = useBlockProps((function () {
                var _tv = getTypoCssVars();
                var s = {
                    '--bkbg-pb-bar-height': a.barHeight + 'px',
                    '--bkbg-pb-bar-spacing': a.barSpacing + 'px',
                    '--bkbg-pb-bar-radius': a.barRadius + 'px',
                    '--bkbg-pb-track-color': a.trackColor,
                    '--bkbg-pb-label-color': a.labelColor,
                    '--bkbg-pb-percentage-color': a.percentageColor,
                    '--bkbg-pb-title-color': a.titleColor,
                    '--bkbg-pb-animation-duration': a.animationDuration + 'ms'
                };
                if (_tv) {
                    Object.assign(s, _tv(a.titleTypo || {}, '--bkbg-pb-tt-'));
                    Object.assign(s, _tv(a.labelTypo || {}, '--bkbg-pb-lb-'));
                    Object.assign(s, _tv(a.pctTypo || {}, '--bkbg-pb-pct-'));
                }
                return { className: layoutClass, style: s, 'data-layout': a.layoutStyle, 'data-block-label': 'Progress Bar' };
            })());

            // Block toolbar
            var blockToolbar = el(BlockControls, {},
                el(ToolbarGroup, {},
                    el(ToolbarButton, {
                        icon: 'plus-alt2',
                        label: __('Add Progress Bar', 'blockenberg'),
                        onClick: addItem
                    })
                )
            );

            // Add item action bar (shared editor UI)
            var addButton = el('div', { className: 'bkbg-editor-actions' },
                el(Button, {
                    variant: 'secondary',
                    icon: 'plus-alt2',
                    onClick: addItem
                }, __('Add Bar', 'blockenberg'))
            );

            return el(Fragment, {},
                blockToolbar,
                inspector,
                el('div', blockProps,
                    titleEl,
                    el('div', { className: 'bkbg-pb-list' }, progressBars),
                    addButton
                )
            );
        },

        save: function (props) {
            var a = props.attributes;

            // CSS variables
            var wrapStyle = (function () {
                var _tv = getTypoCssVars();
                var s = {
                    '--bkbg-pb-bar-height': a.barHeight + 'px',
                    '--bkbg-pb-bar-spacing': a.barSpacing + 'px',
                    '--bkbg-pb-bar-radius': a.barRadius + 'px',
                    '--bkbg-pb-track-color': a.trackColor,
                    '--bkbg-pb-label-color': a.labelColor,
                    '--bkbg-pb-percentage-color': a.percentageColor,
                    '--bkbg-pb-title-color': a.titleColor,
                    '--bkbg-pb-animation-duration': a.animationDuration + 'ms'
                };
                if (_tv) {
                    Object.assign(s, _tv(a.titleTypo || {}, '--bkbg-pb-tt-'));
                    Object.assign(s, _tv(a.labelTypo || {}, '--bkbg-pb-lb-'));
                    Object.assign(s, _tv(a.pctTypo || {}, '--bkbg-pb-pct-'));
                }
                return s;
            })();

            // Serialize items for frontend
            var itemsData = a.items.map(function (item) {
                return item.label + '|' + item.percentage + '|' + item.color;
            }).join(';;');

            // Layout classes
            var layoutClass = 'bkbg-pb-wrap';
            layoutClass += ' bkbg-pb-' + a.layoutStyle;
            if (a.stripes) layoutClass += ' bkbg-pb-striped';
            if (a.stripesAnimated) layoutClass += ' bkbg-pb-striped-animated';
            if (a.glowEffect) layoutClass += ' bkbg-pb-glow';
            if (a.gradient) layoutClass += ' bkbg-pb-gradient';

            // Title element
            var titleEl = null;
            if (a.showTitle && a.title) {
                titleEl = el('div', { className: 'bkbg-pb-title' }, a.title);
            }

            return el('div', {
                className: layoutClass,
                style: wrapStyle,
                'data-layout': a.layoutStyle,
                'data-items': itemsData,
                'data-animate': a.animateOnScroll ? '1' : '0',
                'data-show-labels': a.showLabels ? '1' : '0',
                'data-show-percentage': a.showPercentage ? '1' : '0',
                'data-percentage-position': a.percentagePosition,
                'data-gradient': a.gradient ? '1' : '0',
                'data-gradient-angle': a.gradientAngle,
                'data-glow': a.glowEffect ? '1' : '0'
            },
                titleEl,
                el('div', { className: 'bkbg-pb-list' })
            );
        }
    });
}() );

```
