code:
import {BB} from '../../../../bb/bb';
import {klHistory} from '../../../history/kl-history';
import {Select} from '../../components/select';
import {PointSlider} from '../../components/point-slider';
import {KlCanvas, MAX_LAYERS} from '../../../canvas/kl-canvas';
import {TMixMode, TUiLayout} from '../../../kl-types';
import {LANG} from '../../../../language/language';
import {translateBlending} from '../../../canvas/translate-blending';
import {PointerListener} from '../../../../bb/input/pointer-listener';
import {IPointerEvent} from '../../../../bb/input/event.types';
import {renameLayerDialog} from './rename-layer-dialog';
import {mergeLayerDialog} from './merge-layer-dialog';
import addLayerImg from '/src/app/img/ui/add-layer.svg';
import duplicateLayerImg from '/src/app/img/ui/duplicate-layer.svg';
import mergeLayerImg from '/src/app/img/ui/merge-layers.svg';
import removeLayerImg from '/src/app/img/ui/remove-layer.svg';
import renameLayerImg from '/src/app/img/ui/rename-layer.svg';
import {theme} from '../../../../theme/theme';
import {throwIfNull} from '../../../../bb/base/base';
import {hasPointerEvents} from '../../../../bb/base/browser';

type TLayerEl = HTMLElement & {
    label: HTMLElement;
    opacityLabel: HTMLElement;
    thumb: HTMLCanvasElement;

    spot: number;
    posY: number;
    layerName: string;
    opacity: number;
    pointerListener: PointerListener;
    opacitySlider: PointSlider;
    isSelected: boolean;
};

const paddingLeft = 25;

export class LayerManager {

    private readonly parentEl: HTMLElement;
    private readonly rootEl: HTMLElement;
    private readonly klCanvas: KlCanvas;
    private klCanvasLayerArr: {
        context: CanvasRenderingContext2D;
        opacity: number;
        name: string;
        mixModeStr: TMixMode;
    }[];
    private readonly layerListEl: HTMLElement;
    private layerElArr: TLayerEl[];
    private selectedSpotIndex: number;
    private readonly removeBtn: HTMLButtonElement;
    private readonly addBtn: HTMLButtonElement;
    private readonly duplicateBtn: HTMLButtonElement;
    private readonly mergeBtn: HTMLButtonElement;
    private readonly modeSelect: Select<TMixMode>;
    private uiState: TUiLayout;
    private readonly largeThumbDiv: HTMLElement;
    private oldHistoryState: number | undefined;

    private readonly largeThumbCanvas: HTMLCanvasElement;
    private largeThumbInDocument: boolean;
    private largeThumbInTimeout: undefined | ReturnType<typeof setTimeout>;
    private largeThumbTimeout: undefined | ReturnType<typeof setTimeout>;
    private lastpos: number = 0;

    private readonly onSelect: (layerIndex: number) => void;
    private readonly layerHeight: number = 35;
    private readonly layerSpacing: number = 0;

    private move (oldSpotIndex: number, newSpotIndex: number): void {
        if (isNaN(oldSpotIndex) || isNaN(newSpotIndex)) {
            throw 'layermanager - invalid move';
        }
        for (let i = 0; i < this.klCanvasLayerArr.length; i++) {
            ((i) => {
                let posy = this.layerElArr[i].spot; // <- here
                if (this.layerElArr[i].spot === oldSpotIndex) {
                    posy = newSpotIndex;
                } else {
                    if (this.layerElArr[i].spot > oldSpotIndex) {
                        posy--;
                    }
                    if (posy >= newSpotIndex) {
                        posy++;
                    }
                }
                this.layerElArr[i].spot = posy;
                this.layerElArr[i].posY = (this.layerHeight + this.layerSpacing) * (this.klCanvasLayerArr.length - posy - 1);
                this.layerElArr[i].style.top = this.layerElArr[i].posY + 'px';
            })(i);
        }
        if (oldSpotIndex === newSpotIndex) {
            return;
        }
        this.klCanvas.moveLayer(this.selectedSpotIndex, newSpotIndex - oldSpotIndex);
        this.klCanvasLayerArr = this.klCanvas.getLayers();
        this.selectedSpotIndex = newSpotIndex;
        this.mergeBtn.disabled = this.selectedSpotIndex === 0;
    }

    private posToSpot (p: number): number {
        let result = parseInt('' + (p / (this.layerHeight + this.layerSpacing) + 0.5));
        result = Math.min(this.klCanvasLayerArr.length - 1, Math.max(0, result));
        result = this.klCanvasLayerArr.length - result - 1;
        return result;
    }

    /**
     * update css position of all layers that are not being dragged, while dragging
     */
    private updateLayersVerticalPosition (id: number, newspot: number): void {
        newspot = Math.min(this.klCanvasLayerArr.length - 1, Math.max(0, newspot));
        if (newspot === this.lastpos) {
            return;
        }
        for (let i = 0; i < this.klCanvasLayerArr.length; i++) {
            if (this.layerElArr[i].spot === id) { // <- here
                continue;
            }
            let posy = this.layerElArr[i].spot;
            if (this.layerElArr[i].spot > id) {
                posy--;
            }
            if (posy >= newspot) {
                posy++;
            }
            this.layerElArr[i].posY = (this.layerHeight + this.layerSpacing) * (this.klCanvasLayerArr.length - posy - 1);
            this.layerElArr[i].style.top = this.layerElArr[i].posY + 'px';
        }
        this.lastpos = newspot;
    }

    private renameLayer (layerSpot: number): void {

        renameLayerDialog(
            this.parentEl,
            this.klCanvas.getLayer(layerSpot)!.name,
            (newName) => {
                if (newName === undefined || newName === this.klCanvas.getLayer(layerSpot)!.name) {
                    return;
                }
                this.klCanvas.renameLayer(layerSpot, newName);
                this.createLayerList();
                klHistory.pause(true);
                this.onSelect(layerSpot);
                klHistory.pause(false);
            }
        );
    }

    private updateHeight (): void {
        this.layerListEl.style.height = (this.layerElArr.length * 35) + 'px';
    }

    private createLayerList (): void {
        this.oldHistoryState = klHistory.getState();
        this.klCanvasLayerArr = this.klCanvas.getLayers();
        const checkerImUrl = BB.createCheckerDataUrl(4, undefined, theme.isDark());

        const createLayerEntry = (index: number): void => {
            const klLayer = throwIfNull(this.klCanvas.getLayer(index));
            const layerName = klLayer.name;
            const opacity = this.klCanvasLayerArr[index].opacity;
            const isVisible = klLayer.isVisible;
            const layercanvas = this.klCanvasLayerArr[index].context.canvas;

            const layer: TLayerEl = BB.el({
                className: 'kl-layer',
            }) as HTMLElement as TLayerEl;
            this.layerElArr[index] = layer;
            layer.posY = ((this.klCanvasLayerArr.length - 1) * 35 - index * 35);
            BB.css(layer, {
                top: layer.posY + 'px',
            });
            const innerLayer = BB.el();
            BB.css(innerLayer, {
                position: 'relative',
            });

            const container1 = BB.el();
            BB.css(container1, {
                width: '270px',
                height: '34px',
            });
            const container2 = BB.el();
            layer.append(innerLayer);
            innerLayer.append(container1, container2);

            layer.spot = index;

            //checkbox - visibility
            {
                const checkWrapper = BB.el({
                    tagName: 'label',
                    parent: container1,
                    title: LANG('layers-visibility-toggle'),
                    css: {
                        display: 'flex',
                        width: '25px',
                        height: '100%',
                        justifyContent: 'right',
                        alignItems: 'center',
                        cursor: 'pointer',
                    },
                });
                const check = BB.el({
                    tagName: 'input',
                    parent: checkWrapper,
                    custom: {
                        type: 'checkbox',
                        tabindex: '-1',
                    },
                    css: {
                        display: 'block',
                        cursor: 'pointer',
                        margin: '0',
                        marginRight: '5px',
                    },
                });
                check.checked = isVisible;
                check.onchange = () => {
                    this.klCanvas.setLayerIsVisible(layer.spot, check.checked);
                    this.createLayerList();
                    if (layer.spot === this.selectedSpotIndex) {
                        klHistory.pause(true);
                        this.onSelect(this.selectedSpotIndex);
                        klHistory.pause(false);
                    }
                };
                // prevent layer getting dragged
                const preventFunc = (e: PointerEvent | MouseEvent) => {
                    e.preventDefault();
                    e.stopPropagation();
                };
                if (hasPointerEvents) {
                    checkWrapper.onpointerdown = preventFunc;
                } else {
                    checkWrapper.onmousedown = preventFunc;
                }
            }

            //thumb
            {
                const thumbDimensions = BB.fitInto(layercanvas.width, layercanvas.height, 30, 30, 1);
                layer.thumb = BB.canvas(thumbDimensions.width, thumbDimensions.height);

                const thc = BB.ctx(layer.thumb);
                thc.save();
                if (layer.thumb.width > layercanvas.width) {
                    thc.imageSmoothingEnabled = false;
                }
                thc.drawImage(layercanvas, 0, 0, layer.thumb.width, layer.thumb.height);
                thc.restore();
                BB.css(layer.thumb, {
                    position: 'absolute',
                    left: ((32 - layer.thumb.width) / 2 + paddingLeft) + 'px',
                    top: ((32 - layer.thumb.height) / 2 + 1) + 'px',
                });
                layer.thumb.style.backgroundImage = 'url(' + checkerImUrl + ')';
            }

            //layerlabel
            {
                layer.label = BB.el({
                    className: 'kl-layer__label',
                });
                layer.layerName = layerName;
                layer.label.append(layer.layerName);

                BB.css(layer.label, {
                    position: 'absolute',
                    left: (1 + 32 + 5 + paddingLeft) + 'px',
                    top: 1 + 'px',
                    fontSize: '13px',
                    width: '170px',
                    height: '20px',
                    overflow: 'hidden',
                    whiteSpace: 'nowrap',
                });

                layer.label.ondblclick = () => {
                    this.renameLayer(layer.spot);
                };
            }
            //layer label opacity
            {
                layer.opacityLabel = BB.el({
                    className: 'kl-layer__opacity-label',
                });
                layer.opacity = opacity;
                layer.opacityLabel.append(parseInt('' + (layer.opacity * 100)) + '%');

                BB.css(layer.opacityLabel, {
                    position: 'absolute',
                    left: (250 - 1 - 5 - 50 - 5 + paddingLeft) + 'px',
                    top: 1 + 'px',
                    fontSize: '13px',
                    textAlign: 'right',
                    width: '50px',
                    transition: 'color 0.2s ease-in-out',
                    textDecoration: isVisible ? undefined : 'line-through',
                });
            }

            let oldOpacity: number;
            const opacitySlider = new PointSlider({
                init: layer.opacity,
                width: 200,
                pointSize: 14,
                callback: (sliderValue, isFirst, isLast) => {
                    if (isFirst) {
                        oldOpacity = this.klCanvas.getLayer(layer.spot)!.opacity;
                        klHistory.pause(true);
                        return;
                    }
                    if (isLast) {
                        klHistory.pause(false);
                        if (oldOpacity !== sliderValue) {
                            this.klCanvas.layerOpacity(layer.spot, sliderValue);
                        }
                        return;
                    }
                    layer.opacityLabel.innerHTML = Math.round(sliderValue * 100) + '%';
                    this.klCanvas.layerOpacity(layer.spot, sliderValue);
                },
            });
            BB.css(opacitySlider.getEl(), {
                position: 'absolute',
                left: (39 + paddingLeft) + 'px',
                top: '17px',
            });
            layer.opacitySlider = opacitySlider;

            //larger layer preview - hover
            layer.thumb.onpointerover = (e) => {
                if (e.buttons !== 0 && (!e.pointerType || e.pointerType !== 'touch')) { //shouldn't show while dragging
                    return;
                }

                const thumbDimensions = BB.fitInto(layercanvas.width, layercanvas.height, 250, 250, 1);

                if (this.largeThumbCanvas.width !== thumbDimensions.width || this.largeThumbCanvas.height !== thumbDimensions.height) {
                    this.largeThumbCanvas.width = thumbDimensions.width;
                    this.largeThumbCanvas.height = thumbDimensions.height;
                }
                const ctx = BB.ctx(this.largeThumbCanvas);
                ctx.save();
                if (this.largeThumbCanvas.width > layercanvas.width) {
                    ctx.imageSmoothingEnabled = false;
                }
                ctx.imageSmoothingQuality = 'high';
                ctx.clearRect(0, 0, this.largeThumbCanvas.width, this.largeThumbCanvas.height);
                ctx.drawImage(layercanvas, 0, 0, this.largeThumbCanvas.width, this.largeThumbCanvas.height);
                ctx.restore();
                BB.css(this.largeThumbDiv, {
                    top: (e.clientY - this.largeThumbCanvas.height / 2) + 'px',
                    opacity: '0',
                });
                if (!this.largeThumbInDocument) {
                    document.body.append(this.largeThumbDiv);
                    this.largeThumbInDocument = true;
                }
                clearTimeout(this.largeThumbInTimeout);
                this.largeThumbInTimeout = setTimeout(() => {
                    BB.css(this.largeThumbDiv, {
                        opacity: '1',
                    });
                }, 20);
                clearTimeout(this.largeThumbTimeout);
            };
            layer.thumb.onpointerout = () => {
                clearTimeout(this.largeThumbInTimeout);
                BB.css(this.largeThumbDiv, {
                    opacity: '0',
                });
                clearTimeout(this.largeThumbTimeout);
                this.largeThumbTimeout = setTimeout(() => {
                    if (!this.largeThumbInDocument) {
                        return;
                    }
                    this.largeThumbDiv.remove();
                    this.largeThumbInDocument = false;
                }, 300);
            };

            container1.append(layer.thumb, layer.label, layer.opacityLabel, opacitySlider.getEl());
            let dragstart = false;
            let freshSelection = false;

            //events for moving layers up and down
            const dragEventHandler = (event: IPointerEvent) => {
                if (event.type === 'pointerdown' && event.button === 'left') {
                    BB.css(layer, {
                        transition: 'box-shadow 0.3s ease-in-out',
                    });
                    layer.style.zIndex = '1';
                    this.lastpos = layer.spot;
                    freshSelection = false;
                    if (!layer.isSelected) {
                        freshSelection = true;
                        this.activateLayer(layer.spot);
                    }
                    dragstart = true;

                } else if (event.type === 'pointermove' && event.button === 'left') {

                    if (dragstart) {
                        dragstart = false;
                        BB.css(layer, {
                            boxShadow: '1px 3px 5px rgba(0,0,0,0.4)',
                        });
                    }
                    layer.posY += event.dY;
                    const corrected = Math.max(0, Math.min((this.klCanvasLayerArr.length - 1) * (35), layer.posY));
                    layer.style.top = corrected + 'px';
                    this.updateLayersVerticalPosition(layer.spot, this.posToSpot(layer.posY));

                }
                if (event.type === 'pointerup') {
                    BB.css(layer, {
                        transition: 'all 0.1s linear',
                    });
                    setTimeout(() => {
                        BB.css(layer, {
                            boxShadow: '',
                        });
                    }, 20);
                    layer.posY = Math.max(0, Math.min((this.klCanvasLayerArr.length - 1) * (35), layer.posY));
                    layer.style.zIndex = '';
                    const newSpot = this.posToSpot(layer.posY);
                    const oldSpot = layer.spot;
                    this.move(layer.spot, newSpot);
                    if (oldSpot != newSpot) {
                        klHistory.pause(true);
                        this.onSelect(this.selectedSpotIndex);
                        klHistory.pause(false);
                    }
                    if (oldSpot === newSpot && freshSelection) {
                        this.onSelect(this.selectedSpotIndex);
                    }
                    freshSelection = false;
                }
            };

            layer.pointerListener = new BB.PointerListener({
                target: container1,
                onPointer: dragEventHandler,
            });

            this.layerListEl.append(layer);
        };
        this.layerElArr = [];
        while (this.layerListEl.firstChild) {
            const child = this.layerListEl.firstChild as TLayerEl;
            child.pointerListener.destroy();
            child.opacitySlider.destroy();
            child.remove();
        }
        for (let i = 0; i < this.klCanvasLayerArr.length; i++) {
            createLayerEntry(i);
        }
        this.activateLayer(this.selectedSpotIndex);
        this.updateHeight();
    }

    // ---- public ----
    constructor (
        klCanvas: KlCanvas,
        onSelect: (layerIndex: number) => void,
        parentEl: HTMLElement,
        uiState: TUiLayout,
    ) {
        this.parentEl = parentEl;
        this.klCanvas = klCanvas;
        this.layerElArr = [];
        this.layerHeight = 35;
        this.layerSpacing = 0;
        const width = 270;
        this.onSelect = onSelect;
        this.uiState = uiState;

        this.largeThumbDiv = BB.el({
            onClick: BB.handleClick,
            css: {
                position: 'absolute',
                top: '500px',
                background: '#aaa',
                boxShadow: '1px 1px 3px rgba(0,0,0,0.3)',
                pointerEvents: 'none',
                padding: '0',
                border: '1px solid #aaa',
                transition: 'opacity 0.3s ease-out',
                userSelect: 'none',
            },
        });
        this.setUiState(uiState);
        BB.createCheckerDataUrl(4, (url) => {
            this.largeThumbDiv.style.backgroundImage = 'url(' + url + ')';
        }, theme.isDark());
        this.largeThumbCanvas = BB.canvas(200, 200);
        this.largeThumbCanvas.style.display = 'block';
        this.largeThumbDiv.append(this.largeThumbCanvas);
        this.largeThumbInDocument = false;

        this.klCanvasLayerArr = this.klCanvas.getLayers();
        this.selectedSpotIndex = this.klCanvasLayerArr.length - 1;
        this.rootEl = BB.el({
            css: {
                marginRight: '10px',
                marginBottom: '10px',
                marginLeft: '10px',
                marginTop: '10px',
                cursor: 'default',
            },
        });

        const listDiv = BB.el({
            css: {
                width: width + 'px',
                position: 'relative',
                margin: '0 -10px',
                zIndex: '0',
            },
        });

        this.layerListEl = BB.el({
            parent: listDiv,
        });

        this.addBtn = BB.el({tagName: 'button'});
        this.duplicateBtn = BB.el({tagName: 'button'});
        this.mergeBtn = BB.el({tagName: 'button'});
        this.removeBtn = BB.el({tagName: 'button'});
        const renameBtn = BB.el({tagName: 'button'});

        const createButtons = () => {
            const div = BB.el();
            const async = () => {
                BB.makeUnfocusable(this.addBtn);
                BB.makeUnfocusable(this.duplicateBtn);
                BB.makeUnfocusable(this.mergeBtn);
                BB.makeUnfocusable(this.removeBtn);
                BB.makeUnfocusable(renameBtn);

                this.addBtn.style.cssFloat = 'left';
                this.duplicateBtn.style.cssFloat = 'left';
                this.mergeBtn.style.cssFloat = 'left';
                this.removeBtn.style.cssFloat = 'left';
                renameBtn.style.cssFloat = 'left';

                this.addBtn.title = LANG('layers-new');
                this.duplicateBtn.title = LANG('layers-duplicate');
                this.removeBtn.title = LANG('layers-remove');
                this.mergeBtn.title = LANG('layers-merge');
                renameBtn.title = LANG('layers-rename-title');

                this.addBtn.style.paddingLeft = '5px';
                this.addBtn.style.paddingRight = '3px';

                this.removeBtn.style.paddingLeft = '5px';
                this.removeBtn.style.paddingRight = '3px';

                this.duplicateBtn.style.paddingLeft = '5px';
                this.duplicateBtn.style.paddingRight = '3px';

                this.mergeBtn.style.paddingLeft = '5px';
                this.mergeBtn.style.paddingRight = '3px';

                renameBtn.style.height = '30px';
                renameBtn.style.lineHeight = '20px';

                this.addBtn.innerHTML = "<img src='" + addLayerImg + "' height='20'/>";
                this.duplicateBtn.innerHTML = "<img src='" + duplicateLayerImg + "' height='20'/>";
                this.mergeBtn.innerHTML = "<img src='" + mergeLayerImg + "' height='20'/>";
                this.removeBtn.innerHTML = "<img src='" + removeLayerImg + "' height='20'/>";
                renameBtn.innerHTML = "<img src='" + renameLayerImg + "' height='20'/>";
                this.addBtn.style.marginRight = '5px';
                this.removeBtn.style.marginRight = '5px';
                this.duplicateBtn.style.marginRight = '5px';
                this.mergeBtn.style.marginRight = '5px';
                div.append(
                    this.addBtn,
                    this.removeBtn,
                    this.duplicateBtn,
                    this.mergeBtn,
                    renameBtn,
                    BB.el({
                        css: {
                            clear: 'both',
                            height: '10px',
                        },
                    }),
                );

                this.addBtn.onclick = () => {
                    if (this.klCanvas.addLayer(this.selectedSpotIndex) === false) {
                        return;
                    }
                    this.klCanvasLayerArr = this.klCanvas.getLayers();

                    if (this.klCanvasLayerArr.length === MAX_LAYERS) {
                        this.addBtn.disabled = true;
                        this.duplicateBtn.disabled = true;
                    }
                    this.removeBtn.disabled = false;
                    this.selectedSpotIndex = this.selectedSpotIndex + 1;
                    this.createLayerList();
                    klHistory.pause(true);
                    this.onSelect(this.selectedSpotIndex);
                    klHistory.pause(false);
                };
                this.duplicateBtn.onclick = () => {
                    if (this.klCanvas.duplicateLayer(this.selectedSpotIndex) === false) {
                        return;
                    }
                    this.klCanvasLayerArr = this.klCanvas.getLayers();
                    if (this.klCanvasLayerArr.length === MAX_LAYERS) {
                        this.addBtn.disabled = true;
                        this.duplicateBtn.disabled = true;
                    }
                    this.removeBtn.disabled = false;
                    this.selectedSpotIndex++;
                    this.createLayerList();
                    klHistory.pause(true);
                    this.onSelect(this.selectedSpotIndex);
                    klHistory.pause(false);
                };
                this.removeBtn.onclick = () => {
                    if (this.layerElArr.length <= 1) {
                        return;
                    }

                    this.klCanvas.removeLayer(this.selectedSpotIndex);
                    if (this.selectedSpotIndex > 0) {
                        this.selectedSpotIndex--;
                    }
                    this.klCanvasLayerArr = this.klCanvas.getLayers();
                    this.createLayerList();
                    klHistory.pause(true);
                    this.onSelect(this.selectedSpotIndex);
                    klHistory.pause(false);
                    if (this.klCanvasLayerArr.length === 1) {
                        this.removeBtn.disabled = true;
                    }
                    if (this.klCanvasLayerArr.length < MAX_LAYERS) {
                        this.addBtn.disabled = false;
                        this.duplicateBtn.disabled = false;
                    }
                };
                this.mergeBtn.onclick = () => {
                    if (this.selectedSpotIndex <= 0) {
                        return;
                    }
                    mergeLayerDialog(this.parentEl, {
                        topCanvas: this.klCanvasLayerArr[this.selectedSpotIndex].context.canvas,
                        bottomCanvas: this.klCanvasLayerArr[this.selectedSpotIndex - 1].context.canvas,
                        topOpacity: this.klCanvas.getLayer(this.selectedSpotIndex)!.opacity,
                        mixModeStr: this.klCanvasLayerArr[this.selectedSpotIndex].mixModeStr,
                        callback: (mode) => {
                            this.klCanvas.mergeLayers(this.selectedSpotIndex, this.selectedSpotIndex - 1, mode as TMixMode | 'as-alpha');
                            this.klCanvasLayerArr = this.klCanvas.getLayers();
                            this.selectedSpotIndex--;
                            if (this.klCanvasLayerArr.length === 1) {
                                this.removeBtn.disabled = true;
                            }
                            if (this.klCanvasLayerArr.length < MAX_LAYERS) {
                                this.addBtn.disabled = false;
                                this.duplicateBtn.disabled = false;
                            }
                            this.createLayerList();
                            klHistory.pause(true);
                            this.onSelect(this.selectedSpotIndex);
                            klHistory.pause(false);
                        },
                    });
                };

                renameBtn.onclick = () => {
                    this.renameLayer(this.selectedSpotIndex);
                };
            };
            setTimeout(async, 1);
            return div;
        };
        this.rootEl.append(createButtons());

        let modeWrapper;
        {
            modeWrapper = BB.el({
                content: LANG('layers-blending') + ' ',
                css: {
                    fontSize: '15px',
                },
            });

            this.modeSelect = new Select<TMixMode>({
                optionArr: [
                    'source-over',
                    undefined,
                    'darken',
                    'multiply',
                    'color-burn',
                    undefined,
                    'lighten',
                    'screen',
                    'color-dodge',
                    undefined,
                    'overlay',
                    'soft-light',
                    'hard-light',
                    undefined,
                    'difference',
                    'exclusion',
                    undefined,
                    'hue',
                    'saturation',
                    'color',
                    'luminosity',
                ].map((item: any) => {
                    return item ? [item, translateBlending(item)] : undefined;
                }),
                onChange: (val) => {
                    this.klCanvas.setMixMode(this.selectedSpotIndex, val as TMixMode);
                    this.update(this.selectedSpotIndex);
                },
                css: {
                    marginBottom: '10px',
                },
            });

            modeWrapper.append(this.modeSelect.getElement());
            this.rootEl.append(modeWrapper);

        }

        this.rootEl.append(listDiv);

        //updating the thumbs in interval
        //don't update when: manager not visible || layer didn't change || is drawing
        setInterval(() => {
            if (this.rootEl.style.display !== 'block') {
                return;
            }

            const historyState = klHistory.getState();
            if (historyState === this.oldHistoryState) {
                return;
            }
            this.oldHistoryState = historyState;

            for (let i = 0; i < this.layerElArr.length; i++) {
                if (this.selectedSpotIndex === this.layerElArr[i].spot && this.klCanvasLayerArr[this.layerElArr[i].spot]) { // second check, because might be out of date
                    const ctx = BB.ctx(this.layerElArr[i].thumb);
                    ctx.save();
                    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                    if (this.klCanvasLayerArr[this.layerElArr[i].spot].context.canvas.width < this.layerElArr[i].thumb.width) {
                        ctx.imageSmoothingEnabled = false;
                    }
                    ctx.drawImage(this.klCanvasLayerArr[this.layerElArr[i].spot].context.canvas, 0, 0, this.layerElArr[i].thumb.width, this.layerElArr[i].thumb.height);
                    ctx.restore();
                }
            }

        }, 1);

        theme.addIsDarkListener(() => {
            this.createLayerList();
        });

        this.createLayerList();
    }

    // ---- interface ----
    update (activeLayerSpotIndex?: number): void {
        this.klCanvasLayerArr = this.klCanvas.getLayers();
        if (activeLayerSpotIndex || activeLayerSpotIndex === 0) {
            this.selectedSpotIndex = activeLayerSpotIndex;
        }
        this.removeBtn.disabled = this.klCanvasLayerArr.length === 1;
        if (this.klCanvasLayerArr.length === MAX_LAYERS) {
            this.addBtn.disabled = true;
            this.duplicateBtn.disabled = true;
        } else {
            this.addBtn.disabled = false;
            this.duplicateBtn.disabled = false;
        }
        setTimeout(() => this.createLayerList(), 1);
    }

    getSelected (): number {
        return this.selectedSpotIndex;
    }

    activateLayer (spotIndex: number): void {
        if (spotIndex < 0 || spotIndex > this.layerElArr.length - 1) {
            throw 'invalid spotIndex ' + spotIndex + ', layerElArr.length ' + this.layerElArr.length;
        }
        this.selectedSpotIndex = spotIndex;
        this.modeSelect.setValue(this.klCanvasLayerArr[this.selectedSpotIndex].mixModeStr);
        for (let i = 0; i < this.layerElArr.length; i++) {
            const layer = this.layerElArr[i];
            const isSelected = this.selectedSpotIndex === layer.spot;

            BB.css(layer, {
                boxShadow: '',
            });
            layer.classList.toggle('kl-layer--selected', isSelected);
            layer.opacitySlider.setActive(isSelected);
            layer.isSelected = isSelected;
        }
        this.mergeBtn.disabled = this.selectedSpotIndex === 0;
    }

    setUiState (stateStr: TUiLayout): void {
        this.uiState = stateStr;

        if (this.uiState === 'left') {
            BB.css(this.largeThumbDiv, {
                left: '280px',
                right: '',
            });
        } else {
            BB.css(this.largeThumbDiv, {
                left: '',
                right: '280px',
            });
        }
    }

    getElement (): HTMLElement {
        return this.rootEl;
    }

}


code:
import {BB} from '../../../../bb/bb';
import {LANG} from '../../../../language/language';
import {Options} from '../../components/options';
import {translateBlending} from '../../../canvas/translate-blending';
import {showModal} from '../../modals/base/showModal';
import {TMixMode} from '../../../kl-types';
import {theme} from '../../../../theme/theme';

export function mergeLayerDialog (
    parentEl: HTMLElement,
    p: {
        topCanvas: HTMLCanvasElement;
        bottomCanvas: HTMLCanvasElement;
        topOpacity: number;
        mixModeStr: TMixMode;
        callback: (mode: string) => void;
    }
): void {
    const div = BB.el();
    div.innerHTML = LANG('layers-merge-description');

    const options = new Options({
        optionArr: [
            {id: p.mixModeStr, label: translateBlending(p.mixModeStr)},
            {id: 'source-in', label: 'source-in'},
            {id: 'source-out', label: 'source-out'},
            {id: 'source-atop', label: 'source-atop'},
            {id: 'destination-in', label: 'destination-in'},
            {id: 'destination-out', label: 'destination-out'},
            {id: 'destination-atop', label: 'destination-atop'},
            {id: 'xor', label: 'xor'},
        ],
        initId: p.mixModeStr,
        onChange: () => {
            update();
        },
        isSmall: true,
    });
    options.getElement().style.marginTop = '5px';
    div.append(options.getElement());

    const thumbDimensions = BB.fitInto(p.topCanvas.width, p.topCanvas.height, 200, 200, 1);
    const preview = BB.canvas(thumbDimensions.width, thumbDimensions.height);
    preview.title = LANG('preview');
    preview.className = 'kl-merge-preview';
    const spacer = BB.el({
        content: '<br/>',
        css: {
            clear: 'both',
        },
    });
    div.append(spacer, preview);

    function updateCheckerboard (): void {
        BB.createCheckerDataUrl(4, (url) => {
            preview.style.backgroundImage = 'url(' + url + ')';
        }, theme.isDark());
    }
    updateCheckerboard();
    theme.addIsDarkListener(updateCheckerboard);

    const alphaCanvas = BB.copyCanvas(preview);
    BB.ctx(alphaCanvas).drawImage(p.topCanvas, 0, 0, alphaCanvas.width, alphaCanvas.height);
    BB.convertToAlphaChannelCanvas(alphaCanvas);

    const update = () => {
        const ctx = BB.ctx(preview);
        ctx.save();
        ctx.clearRect(0, 0, preview.width, preview.height);
        if (preview.width > p.topCanvas.width) {
            ctx.imageSmoothingEnabled = false;
        }
        ctx.drawImage(p.bottomCanvas, 0, 0, preview.width, preview.height);

        if (options.getValue() === 'as-alpha') {
            ctx.globalCompositeOperation = 'destination-in';
            ctx.globalAlpha = p.topOpacity;
            ctx.drawImage(alphaCanvas, 0, 0, preview.width, preview.height);
        } else {
            ctx.globalCompositeOperation = options.getValue() as GlobalCompositeOperation;
            ctx.globalAlpha = p.topOpacity;
            ctx.drawImage(p.topCanvas, 0, 0, preview.width, preview.height);
        }
        ctx.restore();
    };

    update();

    const keyListener = new BB.KeyListener({
        onDown: (keyStr: string) => {
            if (keyStr === 'right') {
                options.next();
            }
            if (keyStr === 'left') {
                options.previous();
            }
        },
    });

    showModal({
        target: parentEl,
        message: `<b>${LANG('layers-merge-modal-title')}</b>`,
        div: div,
        buttons: ['Ok', 'Cancel'],
        clickOnEnter: 'Ok',
        callback: (val) => {
            keyListener.destroy();
            options.destroy();
            theme.removeIsDarkListener(updateCheckerboard);
            if (val === 'Ok') {
                p.callback(options.getValue());
            }
        },
    });
}

repo_name: Doggermelon/klecks

license: mit


code:
import {BB} from '../../../../bb/bb';
import {LANG} from '../../../../language/language';
import {showModal} from '../../modals/base/showModal';
import removeLayerImg from '/src/app/img/ui/remove-layer.svg';

export function renameLayerDialog (
    parentEl: HTMLElement,
    currentName: string,
    callback: (newName: string | undefined) => void
): void {
    const div = BB.el();

    const label = BB.el({
        content: LANG('layers-rename-name') + ':',
        css: {
            marginRight: '5px',
        },
    });

    const row = BB.el({
        css: {
            display: 'flex',
        },
    });
    const input = BB.el({tagName: 'input'});
    input.value = currentName;
    input.setAttribute('data-ignore-focus', 'true');
    const clearBtn = BB.el({
        tagName: 'button',
        content: '<img src="' + removeLayerImg + '" height="20"/>',
        title: LANG('layers-rename-clear'),
        css: {
            marginLeft: '10px',
        },
        onClick: () => {
            input.value = '';
            input.focus();
        },
    });
    const suggestions = [
        LANG('layers-rename-sketch'),
        LANG('layers-rename-colors'),
        LANG('layers-rename-shading'),
        LANG('layers-rename-lines'),
        LANG('layers-rename-effects'),
        LANG('background'),
        LANG('layers-rename-foreground'),
    ];
    const suggestionBtns: HTMLButtonElement[] = [];
    const row2 = BB.el({
        css: {
            display: 'flex',
            flexWrap: 'wrap',
            marginTop: '5px',
            marginLeft: '-5px',
        },
    });
    suggestions.forEach(item => {
        const btn = BB.el({
            parent: row2,
            tagName: 'button',
            content: item,
            onClick: () => {
                input.value = '' + btn.textContent;
            },
            css: {
                margin: '5px 0 0 5px',
            },
        });
        suggestionBtns.push(btn);
    });

    div.append(label);
    label.append(row, row2);
    row.append(input, clearBtn);

    setTimeout(() => {
        input.focus();
        input.select();
    }, 10);

    showModal({
        target: parentEl,
        message: `<b>${LANG('layers-rename-title')}</b>`,
        div: div,
        buttons: [LANG('layers-rename'), 'Cancel'],
        primaries: [LANG('layers-rename')],
        callback: (val) => {
            BB.destroyEl(clearBtn);
            suggestionBtns.forEach(item => {
                BB.destroyEl(item);
            });
            suggestionBtns.splice(0, suggestionBtns.length);
            if (val === LANG('layers-rename')) {
                callback(input.value);
            } else {
                callback(undefined);
            }
        },
        clickOnEnter: LANG('layers-rename'),
    });
}


code:
import {BB} from '../../../bb/bb';
import {LANG, languageStrings, LS_LANGUAGE_KEY} from '../../../language/language';
import {KL} from '../../kl';
import {languages} from '../../../../languages/languages';
import bitbofLogoImg from '/src/app/img/bitbof-logo.svg';
import klecksLogoImg from '/src/app/img/klecks-logo.png';
import uiSwapImg from '/src/app/img/ui/ui-swap-lr.svg';
import {LocalStorage} from '../../../bb/base/local-storage';
import {theme, TTheme} from '../../../theme/theme';
import {addIsDarkListener, nullToUndefined} from '../../../bb/base/base';
import {showLicensesDialog} from '../modals/licenses-dialog/show-licenses-dialog';

export class SettingsTab {

    private el: HTMLElement;

    // --- public ---
    constructor (
        onLeftRight: () => void,
        customAbout?: HTMLElement,
    ) {
        this.el = BB.el({
            css: {
                margin: '10px',
            },
        });

        // ---- language ----
        const autoLanguage = languageStrings.getAutoLanguage();

        const langWrapper = BB.el({
            parent: this.el,
            content: BB.el({
                content: LANG('settings-language') + ':',
                css: {
                    marginRight: '5px',
                    marginBottom: '2px',
                },
            }),
            css: {
                display: 'flex',
                alignItems: 'center',
                flexWrap: 'wrap',
            },
        });

        const options: [string, string][] = [
            ['auto', LANG('auto') + ` → ${autoLanguage.name} (${autoLanguage.code})`] as [string, string],
            ...languages.map(item => {
                return [item.code, item.name + ` (${item.code})`] as [string, string];
            }),
        ];
        const languageSelect = new KL.Select({
            initValue: nullToUndefined(LocalStorage.getItem(LS_LANGUAGE_KEY) ? LocalStorage.getItem(LS_LANGUAGE_KEY) : 'auto'),
            optionArr: options,
            onChange: (val) => {
                if (val === 'auto') {
                    LocalStorage.removeItem(LS_LANGUAGE_KEY);
                } else {
                    LocalStorage.setItem(LS_LANGUAGE_KEY, val);
                }
                languageHint.style.display = 'block';
            },
        });
        BB.css(languageSelect.getElement(), {
            flexGrow: '1',
        });
        const languageHint = BB.el({
            className: 'kl-toolspace-note',
            content: LANG('settings-language-reload'),
            css: {
                display: 'none',
                marginTop: '5px',
                flexGrow: '1',
            },
        });

        langWrapper.append(
            languageSelect.getElement(),
            languageHint,
        );

        // ---- theme ----
        function themeToLabel (theme: TTheme): string {
            return theme === 'dark' ? '⬛ ' + LANG('theme-dark') : '⬜ ' + LANG('theme-light');
        }
        const themeSelect = new KL.Select({
            optionArr: [
                ['auto', LANG('auto') + ' → ' + themeToLabel(theme.getMediaQueryTheme())],
                ['light', themeToLabel('light')],
                ['dark', themeToLabel('dark')],
            ],
            initValue: theme.getStoredTheme() || 'auto',
            onChange: (val): void => {
                theme.setStoredTheme(val === 'auto' ? undefined : val);
            },
        });
        BB.css(themeSelect.getElement(), {
            flexGrow: '1',
        });
        addIsDarkListener(() => {
            themeSelect.updateLabel('auto', LANG('auto') + ' → ' + themeToLabel(theme.getMediaQueryTheme()));
        });
        BB.el({
            parent: this.el,
            content: [
                BB.el({
                    content: LANG('settings-theme') + ':',
                    css: {
                        marginRight: '5px',
                        marginBottom: '2px',
                    },
                }),
                themeSelect.getElement(),
            ],
            css: {
                marginTop: '15px',
                display: 'flex',
                alignItems: 'center',
                flexWrap: 'wrap',
            },
        });

        // ---- flip ui ----
        BB.el({
            tagName: 'button',
            parent: this.el,
            content: '' + LANG('switch-ui-left-right'),
            onClick: () => onLeftRight(),
            css: {
                marginTop: '15px',
            },
            custom: {
                tabIndex: '-1',
            },
        });

        // ---- about ----
        this.el.append(BB.el({className: 'grid-hr', css: {margin: '10px 0'}}));

        function makeLicenses () {
            return BB.el({
                tagName: 'a',
                content: LANG('licenses'),
                onClick: () => showLicensesDialog(),
            });
        }

        if (customAbout) {
            this.el.append(customAbout);
            if (!customAbout.innerHTML) {
                const minimalAbout = BB.el({
                    parent: customAbout,
                    css: {
                       textAlign: 'center',
                   },
                });
                minimalAbout.append(
                    BB.el({
                        content: `<img alt="icon" height="20" style="vertical-align:middle" src="${bitbofLogoImg}"> <a href="https://bitbof.com" target="_blank" tabIndex="-1">bitbof</a> © 2023<br>`,
                    }),
                    makeLicenses(),
                );
            }
        } else {
            const versionEl = BB.el({
                parent: this.el,
                css: {
                    textAlign: 'center',
                },
                content: `

 bitbof © 2023
`,
            });

            versionEl.append(
                makeLicenses(),
                document.createTextNode(' | '),
                BB.el({
                    tagName: 'a',
                    content: LANG('donate'),
                    custom: {
                        href: 'https://kleki.com/donate/',
                        target: '_blank',
                    },
                }),
                document.createTextNode(' | '),
                BB.el({
                    tagName: 'a',
                    content: LANG('source-code'),
                    custom: {
                        href: 'https://klecks.org',
                        target: '_blank',
                    },
                }),
            );

        }

        window.addEventListener('storage', (e) => {
            if (e.key !== LS_LANGUAGE_KEY) {
                return;
            }
            languageSelect.setValue(
                nullToUndefined(LocalStorage.getItem(LS_LANGUAGE_KEY) ? LocalStorage.getItem(LS_LANGUAGE_KEY) : 'auto'),
            );
        });

    }

    getElement (): HTMLElement {
        return this.el;
    }

}


code:
import {BB} from '../../../bb/bb';
import {Options} from '../components/options';
import {Checkbox} from '../components/checkbox';
import {KlSlider} from '../components/kl-slider';
import {LANG} from '../../../language/language';
import {TShapeToolMode, TShapeToolType} from '../../kl-types';
import {KlColorSlider} from '../components/kl-color-slider';

/**
 * Shape Tool tab contents
 */
export class ShapeUi {

    private readonly rootEl: HTMLElement;
    private isVisible: boolean;
    private readonly colorDiv: HTMLElement;
    private readonly colorSlider: KlColorSlider;
    private shape: TShapeToolType = 'rect';
    private mode: TShapeToolMode = 'stroke';
    private readonly eraserToggle: Checkbox;
    private readonly opacitySlider: KlSlider;
    private readonly lineWidthSlider: KlSlider;
    private readonly outwardsToggle: Checkbox;
    private readonly fixedToggle: Checkbox;
    private readonly snapToggle:Checkbox;
    private readonly lockAlphaToggle: Checkbox;

    // ---- public ----

    constructor (
        p: {
            colorSlider: KlColorSlider; // when opening tab, inserts it (snatches it from where else it was)
        }
    ) {
        this.rootEl = BB.el({
            css: {
                margin: '10px',
            },
        });
        this.isVisible = true;

        this.colorDiv = BB.el({
            parent: this.rootEl,
            css: {
                marginBottom: '10px',
            },
        });

        this.colorSlider = p.colorSlider;

        const previewSize = 35;
        const previewPadding = 8;

        const rectStrokeSvgRect = BB.createSvg({
            elementType: 'rect',
            x: '' + previewPadding,
            width: '' + (previewSize - previewPadding * 2),
        });
        const rectStrokeSvg = BB.createSvg({
            elementType: 'svg',
            width: '' + previewSize,
            height: '' + previewSize,
        });
        rectStrokeSvg.classList.add('dark-invert');
        rectStrokeSvg.append(rectStrokeSvgRect);
        BB.css(rectStrokeSvg, {
            display: 'block',
        });

        const rectFilledSvgRect = BB.createSvg({
            elementType: 'rect',
            x: '' + previewPadding,
            width: '' + (previewSize - previewPadding * 2),
        });
        const rectFilledSvg = BB.createSvg({
            elementType: 'svg',
            width: '' + previewSize,
            height: '' + previewSize,
        });
        rectFilledSvg.classList.add('dark-invert');
        rectFilledSvg.append(rectFilledSvgRect);
        BB.css(rectFilledSvg, {
            display: 'block',
        });

        const ellipseStrokeSvgEllipse = BB.createSvg({
            elementType: 'ellipse',
            cx: '' + (previewSize / 2),
            cy: '' + (previewSize / 2),
            rx: '' + (previewSize / 2 - previewPadding),
        });
        const ellipseStrokeSvg = BB.createSvg({
            elementType: 'svg',
            width: '' + previewSize,
            height: '' + previewSize,
        });
        ellipseStrokeSvg.classList.add('dark-invert');
        ellipseStrokeSvg.append(ellipseStrokeSvgEllipse);
        BB.css(ellipseStrokeSvg, {
            display: 'block',
        });

        const ellipseFilledSvgEllipse = BB.createSvg({
            elementType: 'ellipse',
            cx: '' + (previewSize / 2),
            cy: '' + (previewSize / 2),
            rx: '' + (previewSize / 2 - previewPadding),
        });
        const ellipseFilledSvg = BB.createSvg({
            elementType: 'svg',
            width: '' + previewSize,
            height: '' + previewSize,
        });
        ellipseFilledSvg.classList.add('dark-invert');
        ellipseFilledSvg.append(ellipseFilledSvgEllipse);
        BB.css(ellipseFilledSvg, {
            display: 'block',
        });

        const lineSvgLine = BB.createSvg({
            elementType: 'line',
            x1: '' + previewPadding,
            x2: '' + (previewSize - previewPadding),
        });
        const lineSvg = BB.createSvg({
            elementType: 'svg',
            width: '' + previewSize,
            height: '' + previewSize,
        });
        lineSvg.classList.add('dark-invert');
        lineSvg.append(lineSvgLine);
        BB.css(lineSvg, {
            display: 'block',
        });

        const updatePreviews = () => {
            const strokeWidth = BB.clamp(Math.round(this.lineWidthSlider.getValue() / 10), 1, 10) + 'px';

            const squish = 1.35;

            BB.css(rectStrokeSvgRect, { fill: 'none', stroke: 'black', strokeWidth: strokeWidth });
            BB.css(rectFilledSvgRect, { fill: 'black', stroke: 'none' });

            BB.css(ellipseStrokeSvgEllipse, { fill: 'none', stroke: 'black', strokeWidth: strokeWidth });
            BB.css(ellipseFilledSvgEllipse, { fill: 'black', stroke: 'none' });

            BB.css(
                lineSvgLine,
                { fill: 'none', stroke: 'black', strokeWidth: strokeWidth }
            );

            if (this.fixedToggle.getValue()) {
                rectStrokeSvgRect.setAttribute('y', '' + previewPadding);
                rectStrokeSvgRect.setAttribute('height', '' + (previewSize - previewPadding * 2));
                rectFilledSvgRect.setAttribute('y', '' + previewPadding);
                rectFilledSvgRect.setAttribute('height', '' + (previewSize - previewPadding * 2));

                ellipseStrokeSvgEllipse.setAttribute('ry', '' + (previewSize / 2 - previewPadding));
                ellipseFilledSvgEllipse.setAttribute('ry', '' + (previewSize / 2 - previewPadding));
            } else {
                rectStrokeSvgRect.setAttribute('y', '' + (previewPadding * squish));
                rectStrokeSvgRect.setAttribute('height', '' + (previewSize - previewPadding * squish * 2));
                rectFilledSvgRect.setAttribute('y', '' + (previewPadding * squish));
                rectFilledSvgRect.setAttribute('height', '' + (previewSize - previewPadding * squish * 2));

                ellipseStrokeSvgEllipse.setAttribute('ry', '' + (previewSize / 2 - previewPadding * squish));
                ellipseFilledSvgEllipse.setAttribute('ry', '' + (previewSize / 2 - previewPadding * squish));
            }

            if (this.snapToggle.getValue()) {
                lineSvgLine.setAttribute('y1', '' + (previewSize - previewPadding));
                lineSvgLine.setAttribute('y2', '' + previewPadding);
            } else {
                lineSvgLine.setAttribute('y1', '' + (previewSize - previewPadding * squish));
                lineSvgLine.setAttribute('y2', '' + (previewPadding * squish));
            }
        };

        const row1 = BB.el({
            parent: this.rootEl,
            css: {
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'start',
            },
        });

        const shapeOptions = new Options({
            optionArr: [
                {
                    id: 'rect-stroke',
                    label: rectStrokeSvg,
                    title: LANG('shape-rect') + ' ' + LANG('shape-stroke'),
                },
                {
                    id: 'ellipse-stroke',
                    label: ellipseStrokeSvg,
                    title: LANG('shape-ellipse') + ' ' + LANG('shape-stroke'),
                },
                {
                    id: 'line',
                    label: lineSvg,
                    title: LANG('shape-line'),
                },
                {
                    id: 'rect-fill',
                    label: rectFilledSvg,
                    title: LANG('shape-rect') + ' ' + LANG('shape-fill'),
                },
                {
                    id: 'ellipse-fill',
                    label: ellipseFilledSvg,
                    title: LANG('shape-ellipse') + ' ' + LANG('shape-fill'),
                },
            ],
            initId: this.shape + ' ' + this.mode,
            onChange: (id) => {
                const split = id.split('-');
                this.shape = split[0] as TShapeToolType;
                this.mode = split[1] as TShapeToolMode;

                BB.css(this.fixedToggle.getElement(), {
                    display: this.shape === 'line' ? 'none' : '',
                });
                BB.css(this.snapToggle.getElement(), {
                    display: this.shape === 'line' ? '' : 'none',
                });
                BB.css(this.lineWidthSlider.getElement(), {
                    display: (this.shape !== 'line' && this.mode === 'fill') ? 'none' : '',
                });
            },
            changeOnInit: true,
        });
        shapeOptions.getElement().style.width = '120px';
        row1.append(shapeOptions.getElement());

        this.eraserToggle = new Checkbox({
            init: false,
            label: LANG('eraser'),
            callback: () => {
                updatePreviews();
            },
        });

        this.lockAlphaToggle = new Checkbox({
            init: false,
            label: LANG('lock-alpha'),
            title: LANG('lock-alpha-title'),
            doHighlight: true,
        });
        this.lockAlphaToggle.getElement().style.marginTop = '10px';

        row1.append(BB.el({
            content: [
                this.eraserToggle.getElement(),
                this.lockAlphaToggle.getElement(),
            ],
        }));

        this.lineWidthSlider = new KlSlider({
            label: LANG('shape-line-width'),
            width: 250,
            height: 30,
            min: 1,
            max: 200,
            value: 4,
            curve: 'quadratic',
            onChange: () => {
                updatePreviews();
            },
        });
        BB.css(this.lineWidthSlider.getElement(), {
            marginTop: '10px',
        });
        this.rootEl.append(this.lineWidthSlider.getElement());

        this.opacitySlider = new KlSlider({
            label: LANG('opacity'),
            width: 250,
            height: 30,
            min: 1 / 100,
            max: 1,
            value: 1,
            toValue: (displayValue) => displayValue / 100,
            toDisplayValue: (value) => value * 100,
        });
        BB.css(this.opacitySlider.getElement(), {
            marginTop: '10px',
        });
        this.rootEl.append(this.opacitySlider.getElement());

        const row2 = BB.el({
            parent: this.rootEl,
            css: {
                display: 'flex',
                alignItems: 'center',
                marginTop: '10px',
            },
        });

        this.outwardsToggle = new Checkbox({
            init: false,
            label: LANG('shape-outwards'),
            css: {
                width: '50%',
                marginRight: '10px',
            },
        });
        row2.append(this.outwardsToggle.getElement());

        this.fixedToggle = new Checkbox({
            init: false,
            label: LANG('shape-fixed'),
            callback: () => {
                updatePreviews();
            },
            css: {
                flexGrow: '1',
            },
        });
        row2.append(this.fixedToggle.getElement());

        this.snapToggle = new Checkbox({
            init: false,
            label: LANG('angle-snap'),
            title: LANG('angle-snap-title'),
            callback: () => {
                updatePreviews();
            },
            css: {
                flexGrow: '1',
            },
        });
        row2.append(this.snapToggle.getElement());

        updatePreviews();
    }

    getElement (): HTMLElement {
        return this.rootEl;
    }

    setIsVisible (pIsVisible: boolean): void {
        this.isVisible = !!pIsVisible;
        this.rootEl.style.display = this.isVisible ? 'block' : 'none';
        if (this.isVisible) {
            this.colorDiv.append(this.colorSlider.getElement());
            this.colorDiv.append(this.colorSlider.getOutputElement());
            //update();
        }
    }

    getShape (): TShapeToolType {
        return this.shape;
    }

    getMode (): TShapeToolMode {
        return this.mode;
    }

    getIsEraser (): boolean {
        return this.eraserToggle.getValue();
    }

    getOpacity (): number {
        return this.opacitySlider.getValue();
    }

    getLineWidth (): number {
        return this.lineWidthSlider.getValue();
    }

    getIsOutwards (): boolean {
        return this.outwardsToggle.getValue();
    }

    getIsFixed (): boolean {
        return this.fixedToggle.getValue();
    }

    getIsSnap (): boolean {
        return this.snapToggle.getValue();
    }

    getDoLockAlpha (): boolean {
        return this.lockAlphaToggle.getValue();
    }
}


code:
import {BB} from '../../../bb/bb';
import {LANG} from '../../../language/language';
import {KlColorSlider} from '../components/kl-color-slider';

/**
 * Text Tool tab contents (color slider)
 */
export class TextUi {

    private readonly rootEl: HTMLElement;
    private readonly colorDiv: HTMLElement;
    private isVisible: boolean = true;
    private readonly colorSlider: KlColorSlider;

    // ---- public ----

    constructor (
        p: {
            colorSlider: KlColorSlider; // when opening tab, inserts it (snatches it from where else it was)
        }
    ) {
        this.rootEl = BB.el({
            css: {
                margin: '10px',
            },
        });
        this.colorSlider = p.colorSlider;

        this.colorDiv = BB.el({
            parent: this.rootEl,
            css: {
                marginBottom: '10px',
            },
        });

        const hint = BB.el({
            parent: this.rootEl,
            content: LANG('text-instruction'),
        });
    }

    getElement (): HTMLElement {
        return this.rootEl;
    }

    setIsVisible (pIsVisible: boolean): void {
        this.isVisible = !!pIsVisible;
        this.rootEl.style.display = this.isVisible ? 'block' : 'none';
        if (this.isVisible) {
            this.colorDiv.append(this.colorSlider.getElement(), this.colorSlider.getOutputElement());
        }
    }
}


code:
/**
 * from stabilizer dropdown value to line-smoothing value
 * @param s
 */
export function translateSmoothing (s: number): number {
    if (s == 1) {
        return 1 - 0.5;
    }
    if (s == 2) {
        return 1 - 0.16;
    }
    if (s == 3) {
        return 1 - 0.035;
    }
    if (s == 4) {
        return 1 - 0.0175;
    }
    if (s == 5) {
        return 1 - 0.00875;
    }
    return s;
}


code:
import {english, languages, loadLanguage, TTranslationCode} from '../../languages/languages';

export const LS_LANGUAGE_KEY = 'klecks-language';

class LanguageStrings {
    private data: any;
    private listeners: (() => void)[] = [];
    private code: string;

    // --- public ----
    constructor () {
        // need to use setLanguage for a different language
        this.data = {...english};
        this.code = 'en';
    }

    async setLanguage (langCode: string): Promise<void> {
        if (langCode === 'en') {
            this.data = {...english};
        } else {
            this.data = {...english, ...(await loadLanguage(langCode))};
        }
        this.code = langCode;
        document.documentElement.setAttribute('lang', langCode);
        this.listeners.forEach(item => {
            item();
        });
    }

    get (code: TTranslationCode): string {
        if (!(code in this.data)) {
            throw new Error('translation code doesn\'t exist: ' + code);
        }
        return this.data[code];
    }

    getLanguage (): {code: string; name: string} {
        return languages.find(item => {
            return item.code === this.code;
        })!;
    }

    getAutoLanguage (): {code: string; name: string} {
        const autoCode = getLanguage(false);
        return languages.find(item => {
            return item.code === autoCode;
        })!;
    }

    getCode (): string {
        return this.code;
    }

    // get notified on language change
    subscribe (subscriber: () => void) {
        if (this.listeners.includes(subscriber)) {
            return;
        }
        this.listeners.push(subscriber);
    }

    unsubscribe (subscriber: () => void) {
        for (let i = 0; i < this.listeners.length; i++) {
            if (subscriber === this.listeners[i]) {
                this.listeners.splice(i, 1);
                return;
            }
        }
    }
}

export function getLanguage (useLocalStorage?: boolean): string {

    let result: string = 'en';

    const langs: string[] = []; // from highest to lowest priority
    const navLangs = navigator.languages ? navigator.languages : [navigator.language];
    navLangs.forEach(item => {
        const split = item.split('-');
        langs.push(item);
        if (split.length === 2) {
            langs.push(split[0]);
        }
    });

    if (useLocalStorage) {
        try {
            const item = localStorage.getItem(LS_LANGUAGE_KEY);
            if (item) {
                langs.unshift(item);
            }
        } catch (e) {
            // likely cookies disabled in Safari
        }
    }

    for (let i = 0; i < langs.length; i++) {
        const lang = langs[i];
        const found = languages.find(item => {
            return item.code.toLowerCase() === lang.toLowerCase();
        });
        if (found) {
            result = found.code;
            break;
        }
    }
    return result;
}

const activeLanguageCode = getLanguage(true);
export const languageStrings = new LanguageStrings();

export const LANG = (code: TTranslationCode, replace?: {[key: string]: string}): string => {
    if (replace) {
        let result = languageStrings.get(code);
        const keyArr = Object.keys(replace);
        keyArr.forEach(key => {
            result = result.replace(`{${key}}`, replace[key]);
        });
        return result;
    } else {
        return languageStrings.get(code);
    }
};

export const initLANG = () => languageStrings.setLanguage(activeLanguageCode);


code:
import './polyfills/polyfills';
import {KlApp} from './app/kl-app';
import {IKlProject} from './klecks/kl-types';
import {SaveReminder} from './klecks/ui/components/save-reminder';
import {klHistory} from './klecks/history/kl-history';
import {klPsdToKlProject, readPsd} from './klecks/storage/psd';
import {LANG} from './language/language';
import {loadAgPsd, TAgPsd} from './klecks/storage/load-ag-psd';
import {klConfig} from './klecks/kl-config';

export interface IEmbedParams {
    project?: IKlProject;
    psdBlob?: Blob;
    onSubmit: (onSuccess: () => void, onError: () => void) => void;
    embedUrl: string;
    logoImg?: any;
    bottomBar?: HTMLElement;
    aboutEl?: HTMLElement;
    disableAutoFit?: boolean; // disable automatic Fit to View for small canvases
}

export interface IReadPSD {
    blob: Blob;
    callback: (k: IKlProject | null) => void;
}

/**
 * Note: Wrapped by EmbedWrapper, which quickly provides feedback for the user without having loaded everything.
 * Embed runs when the main bundle is loaded. It instantiates Klecks.
 */
export class Embed {

    private isInitialized: boolean = false;
    private klApp: KlApp | undefined;
    private readonly psdQueue: IReadPSD[] = []; // queue of psds waiting while ag-psd is loading
    private agPsd: TAgPsd | 'error' | undefined;
    private loadingScreenEl: HTMLElement | null;
    private loadingScreenTextEl: HTMLElement | null;

    onProjectReady (project: IKlProject) {
        try {
            if (this.isInitialized) {
                throw new Error('Already called openProject');
            }
            this.isInitialized = true;

            const saveReminder = new SaveReminder(
                klHistory,
                false,
                false,
                () => {},
                () => this.klApp ? this.klApp.isDrawing() : false,
                null,
                null,
            );
            this.klApp = new KlApp(
                project,
                {
                    saveReminder,
                    bottomBar: this.p.bottomBar,
                    aboutEl: this.p.aboutEl,
                    embed: {
                        url: this.p.embedUrl,
                        onSubmit: this.p.onSubmit,
                    },
                }
            );
            saveReminder.init();

            this.loadingScreenEl && this.loadingScreenEl.remove();
            this.loadingScreenEl = null;
            this.loadingScreenTextEl = null;

            document.body.append(this.klApp.getEl());
        } catch (e) {
            if (this.loadingScreenTextEl) {
                this.loadingScreenTextEl.textContent = '❌ ' + e;
            }
            if (this.loadingScreenEl) {
                this.loadingScreenEl.className += 'loading-screen-error';
            }
            console.error(e);
        }
    }

    // ----- public ----------
    constructor (private p: IEmbedParams) {

        this.loadingScreenEl = document.getElementById('loading-screen');
        this.loadingScreenTextEl = document.getElementById('loading-screen-text');
        if (this.loadingScreenTextEl) {
            this.loadingScreenTextEl.textContent = LANG('embed-init-waiting');
        }

        if (p.disableAutoFit) {
            klConfig.disableAutoFit = true;
        }
        if (p.project) {
            this.onProjectReady(p.project);
        }
    }

    openProject: Embed['onProjectReady'] = (project) => {
        this.onProjectReady(project);
    };

    initError (error: string) {
        if (this.loadingScreenTextEl) {
            this.loadingScreenTextEl.textContent = '❌ ' + error;
        }
        if (this.loadingScreenEl) {
            this.loadingScreenEl.className += 'loading-screen-error';
        }
    }

    getPNG (): Blob {
        if (!this.klApp) {
            throw new Error('App not initialized');
        }
        return this.klApp.getPNG();
    }

    async getPSD (): Promise<Blob> {
        if (!this.klApp) {
            throw new Error('App not initialized');
        }
        return await this.klApp.getPSD();
    }

    readPSDs (psds: IReadPSD[]) {
        if (psds.length === 0) {
            return;
        }

        const readItem = (item: IReadPSD) => {
            try {
                const psd = (this.agPsd as any).readPsd(item.blob as any);
                const project = klPsdToKlProject(readPsd(psd));
                item.callback(project);
            } catch (e) {
                console.error('failed to read psd', e);
                item.callback(null);
            }
        };

        // library not loaded yet
        if (!this.agPsd) {
            if (this.psdQueue.length === 0) {
                // load ag-psd
                (async () => {
                    try {
                        this.agPsd = await loadAgPsd();
                    } catch (e) {
                        this.agPsd = 'error';
                    }
                    while (this.psdQueue.length) {
                        readItem(this.psdQueue.shift()!);
                    }
                })();
            }
            psds.forEach(item => {
                this.psdQueue.push(item);
            });
        } else {
            psds.forEach(readItem);
        }
    }

}


code:
/**
 * by bitbof (bitbof.com)
 */

import './polyfills/polyfills';
import {KL} from './klecks/kl';
import {klHistory} from './klecks/history/kl-history';
import {KlApp} from './app/kl-app';
import {IKlProject} from './klecks/kl-types';
import {ProjectStore} from './klecks/storage/project-store';
import {initLANG, LANG} from './language/language';
import '../script/theme/theme';

function initError (e: Error): void {
    const el = document.createElement('div');
    el.style.textAlign = 'center';
    el.style.background = '#fff';
    el.style.padding = '20px';
    el.innerHTML = '
App failed to initialize
';
    const errorMsg = document.createElement('div');
    errorMsg.textContent = 'Error: ' + (e.message ? e.message : ('' + e));
    el.append(errorMsg);
    document.body.append(el);
    console.error(e);
}

(async () => {
    let klApp: KlApp;
    let domIsLoaded = false;

    try {
        window.addEventListener('DOMContentLoaded', () => {
            domIsLoaded = true;
        });
        await initLANG();
    } catch (e) {
        initError(e as Error);
        return;
    }

    function onProjectLoaded (project: IKlProject | null, projectStore: ProjectStore) {
        if (klApp) {
            throw 'onKlProjectObjLoaded called more than once';
        }
        // in case an extension manipulated the page
        const loadingScreenEl = document.getElementById('loading-screen');
        loadingScreenEl?.remove();

        const saveReminder = new KL.SaveReminder(
            klHistory,
            true,
            true,
            () => klApp.saveAsPsd(),
            () => {
                return klApp ? klApp.isDrawing() : false;
            },
            null,
            null,
        );
        klApp = new KlApp(
            project,
            {
                saveReminder,
                projectStore,
            }
        );
        saveReminder.init();
        if (project) {
            setTimeout(() => {
                klApp.out(LANG('file-storage-restored'));
            }, 100);
        }

        document.body.append(klApp.getEl());
    }

    async function onDomLoaded () {
        try {
            window.removeEventListener('DOMContentLoaded', onDomLoaded);
            const projectStore = new KL.ProjectStore();
            let project: IKlProject | null = null;
            try {
                const readResult = await projectStore.read();
                if (readResult) {
                    project = readResult.project;
                }
            } catch(e) {
                let message: string;
                if ((e as Error).message.indexOf('db-error') === 0) {
                    message = 'Failed to access Browser Storage';
                } else if ((e as Error).message.indexOf('format-error') === 0) {
                    message = 'Failed to restore from Browser Storage';
                } else {
                    message = 'Failed to restore from Browser Storage';
                }
                if (message) {
                    setTimeout(function () {
                        klApp && klApp.out(message);
                        throw new Error('Initial browser storage error, ' + e);
                    }, 100);
                }
            }
            onProjectLoaded(project, projectStore);
        } catch (e) {
            initError(e as Error);
        }

    }
    if (domIsLoaded) {
        onDomLoaded();
    } else {
        window.addEventListener('DOMContentLoaded', onDomLoaded);
    }
})();


code:
/**
 * features that were available before module script tags:
 * chrome 61
 * edge 16
 * safari 11
 * firefox 60
 * https://caniuse.com/es6-module
 */

import 'mdn-polyfills/Node.prototype.append';
import 'mdn-polyfills/Node.prototype.prepend'; // edge 17 (one version after module)
import 'mdn-polyfills/String.prototype.padStart';
import 'mdn-polyfills/Array.prototype.findIndex';


code:
import './polyfills-nomodule';

/*
 * ---- Below ----------------------
 * features that didn't have support until *after* browsers supported the modules script tag:
 * Chrome 61
 * Edge 16
 * Safari 11
 * Firefox 60
 * https://caniuse.com/es6-module
 */

// chrome 61, edge 79, safari 16, firefox 107
if (!('scrollTo' in Element.prototype)) {
    Object.defineProperty(Element.prototype, 'scrollTo', {
        value: function (x: number, y: number) {
            this.scrollLeft = x;
            this.scrollTop = y;
        },
    });
}

// chrome 61, edge 79, safari 16, firefox 107
if (!('scrollBy' in Element.prototype)) {
    Object.defineProperty(Element.prototype, 'scrollBy', {
        value: function (x: number, y: number) {
            this.scrollLeft += x;
            this.scrollTop += y;
        },
    });
}

// chrome 69, edge 79, safari 12, firefox 62
if (!Array.prototype.flat) {
    Object.defineProperty(Array.prototype, 'flat', {
        configurable: true,
        value: function flat (...args: any[]) {
            const depth = isNaN(args[0]) ? 1 : Number(args[0]);

            return depth ? Array.prototype.reduce.call(this, function (acc: any, cur) {
                if (Array.isArray(cur)) {
                    // eslint-disable-next-line prefer-spread
                    acc.push.apply(acc, flat.call(cur, depth - 1));
                } else {
                    acc.push(cur);
                }

                return acc;
            }, []) : Array.prototype.slice.call(this);
        },
        writable: true,
    });
}

// Chrome 85, Firefox 77, Safari 13.1
// if there are problems, maybe use core-js
if (!String.prototype.replaceAll) {
    Object.defineProperty(String.prototype, 'replaceAll', {
        value: function (searchValue: string | RegExp, replaceValue: string): string {
            if (typeof replaceValue === 'function') {
                throw new Error('replaceAll polyfill does not support replaceValue: function');
            }
            return this.replace(new RegExp(searchValue, 'g'), replaceValue);
        },
    });
}

/*
    Copyright 2018  Alfredo Mungo <alfredo.mungo@protonmail.ch>

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to
    deal in the Software without restriction, including without limitation the
    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    sell copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    IN THE SOFTWARE.
*/
if (!Object.fromEntries) {
    Object.defineProperty(Object, 'fromEntries', {
        value (entries: any) {
            if (!entries || !entries[Symbol.iterator]) {
                throw new Error('Object.fromEntries() requires a single iterable argument');
            }
            const o: any = {};
            Object.keys(entries).forEach((key) => {
                const [k, v] = entries[key];
                o[k] = v;
            });
            return o;
        },
    });
}

// sometimes Android WebView has no localStorage
if (!('localStorage' in window)) {
    try {
        (window as any).localStorage = {
            getItem: () => null,
            setItem: () => {},
            removeItem: () => {},
        };
    } catch (e) {
        // maybe it fails?
    }
}


code:
import {addIsDarkListener, isDark} from '../bb/base/base';

const LS_THEME_KEY = 'klecks-theme';

export type TTheme = 'dark' | 'light';
type TThemeListener = () => void;

class Theme {

    private storedTheme: TTheme | undefined;
    // initialization will be overwritten.
    private mediaQueryTheme: TTheme = 'light'; // prefers-color-scheme
    private theme: TTheme = 'light';
    private readonly listeners: TThemeListener[] = [];

    private updateTheme (): void {
        const oldTheme = this.theme;
        this.theme = this.storedTheme || this.mediaQueryTheme;

        if (this.theme === oldTheme) {
            return;
        }
        document.body.classList.toggle('kl-theme-dark', this.theme === 'dark');
        this.listeners.forEach(item => item());
    }

    private readLocalStorage (): TTheme | undefined {
        let result = localStorage.getItem(LS_THEME_KEY) as unknown;
        if (!result || (typeof result === 'string' && !['dark', 'light'].includes(result))) {
            result = undefined;
            localStorage.removeItem(LS_THEME_KEY); // reset because invalid
        }
        return result as (TTheme | undefined);
    }

    // ---- public ----
    constructor () {

        // init media query
        this.mediaQueryTheme = isDark() ? 'dark' : 'light';
        addIsDarkListener(() => {
            this.mediaQueryTheme = isDark() ? 'dark' : 'light';
            this.updateTheme();
        });

        // init local storage
        this.storedTheme = this.readLocalStorage();
        addEventListener('storage', (e) => {
            if (e.key === LS_THEME_KEY) {
                this.storedTheme = this.readLocalStorage();
                this.updateTheme();
            }
        });

        this.updateTheme();
    }

    isDark (): boolean {
        return this.theme === 'dark';
    }

    addIsDarkListener (func: TThemeListener): void {
        if (this.listeners.includes(func)) {
            return;
        }
        this.listeners.push(func);
    }

    removeIsDarkListener (func: TThemeListener): void {
        for (let i = 0; i < this.listeners.length; i++) {
            if (this.listeners[i] === func) {
                this.listeners.splice(i, 1);
                return;
            }
        }
    }

    getMediaQueryTheme (): TTheme {
        return this.mediaQueryTheme;
    }

    getStoredTheme (): TTheme | undefined {
        return this.storedTheme;
    }

    setStoredTheme (theme: TTheme | undefined): void {
        if (theme) {
            localStorage.setItem(LS_THEME_KEY, theme);
        } else {
            localStorage.removeItem(LS_THEME_KEY);
        }
        this.storedTheme = theme;
        this.updateTheme();
    }

}

export const theme = new Theme();


code:
$d-bg: #333;

body.kl-theme-dark {
  color-scheme: dark;
  color: #ccc;
  background: rgb(33, 33, 33);

  .kl-toolspace {
    background: $d-bg;
  }

  .kl-toolspace--left {
    border-right: 1px solid #626262;
  }

  .kl-toolspace--right {
    border-left: 1px solid #626262;
  }

  .kl-toolspace-row {
    background: linear-gradient(to top, rgba(255, 255, 255, 0) 20%, rgba(255, 255, 255, 0.1) 100%);
    box-shadow: 0 1px 1px #252525;
  }

  .toolspace-row-button-activated {
    background: #6e6e6e;
  }

  .toolspace-row-button-hover {
    background: #535353;
  }

  .toolspace-row-button:active {
    background: #585858;
  }

  .toolspace-svg-triangle-button-hover {
    fill: #535353;
  }

  .toolspace-svg-triangle-button:active {
    fill: #585858;
  }

  .toolspace-row-button__submit:hover {
    background: #5786ff;
  }

  .tabrow {
    background: transparent;
    border-bottom: 1px solid #6e6e6e;
  }

    .tabrow__tab {
      color: #fff;
    }

    .tabrow__tab--opened, .tabrow__tab--opened-accented {
      background-color: #6e6e6e;
    }

  .tabrow__tab div, .tabrow__tab::after, .tabrow__tab::before {
    filter: brightness(0.45);
  }

  .tabrow__tab:not(.tabrow__tab--opened, .tabrow__tab--opened-accented):hover {
    background-color: #535353;
  }

  .tabrow__tab:active:not(.tabrow__tab--opened-accented, .tabrow__tab--opened) {
    background-color: #585858;
    box-shadow: inset 2px 2px 10px #0003;
  }

  .kl-tool-row-border-right {
    border-right: 1px solid rgb(48, 48, 48);
  }

  .tool-dropdown-wrapper {
    background-color: #5e5e5e;
  }

  .tool-dropdown-button:hover {
    background: #808080;
  }

  .kl-popup {
    background: #212121a1;
  }

  .kl-popup-box {
    background-color: $d-bg;
    box-shadow: 0 0 0 1px #636363;
  }

  .kl-d-modal-root {
    background: #212121a1;
  }

  .kl-d-modal {
    background-color: $d-bg;
    box-shadow: 0 0 0 1px #636363;
  }

  button {
    color: #ccc;
    background-color: #4a4a4a;
    background-image: none;
    box-shadow: none;
    user-select: none;
  }

    button:hover {
      background-color: #555;
      color: #eee;
    }

    button:active {
      background-color: #555;
      color: #fff;
      box-shadow: 0 0 0 1px rgba(255,255,255,0.2);
    }

  button:disabled {
    color: #999;
    background-color: transparent;
    background-image: none;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
    pointer-events: none;
  }

  button:not(.kl-button-primary, .kl-button-cancel) img:not(.dark-no-invert) {
    filter: invert(1) hue-rotate(180deg);
  }

    input, textarea {
      background: #333;
      color: #ccc;
    }

  input:focus, textarea:focus {
    background: #222;
    color: #fff;
  }

  .kl-select {
    background: #222;
    color: #fff;
    border: 2px solid #666;
  }

  .kl-checkbox__inner {
    color: #ccc;

    &:hover {
      color: #fff;
    }
  }

  .kl-checkbox--highlight {
    background: none;
    box-shadow: 0 0 0 1px var(--active-highlight-color);
  }

  .kl-option {
    color: #ccc;
    box-shadow: inset 0 0 0 1px #606060;
  }

  .kl-option-selected {
    color: #fff;
    background: rgba(255, 255, 255, 0.07);
    box-shadow: inset 0 0 0 1px var(--active-highlight-color);
  }

  .kl-box-toggle {
    color: #ccc;
    box-shadow: inset 0 0 0 1px #606060;
  }

  .kl-box-toggle--active {
    color: #fff;
    background: rgba(255, 255, 255, 0.07);
    box-shadow: inset 0 0 0 1px var(--active-highlight-color);
  }

  .dark-invert {
    filter: invert(1);
  }

    .kl-file-no-autosave {
      background: transparent;
      color: #c9b184;
      box-shadow: inset 0 0 0 1px;
    }

  .slider-wrapper {
    background: #222;
    box-shadow: 0 0 0 1px #777;
    color: #ddd;
    text-shadow: none;

    .slider-inner {
      background: #555;
      box-shadow: 1px 0 0 #777;
    }
  }

  .slider-wrapper--active {
    box-shadow: 0 0 0 1px #aaa;
    color: #fff;

    .slider-inner {
      box-shadow: 1px 0 0 #aaa;
    }
  }

  .slider-wrapper--disabled {
    box-shadow: none;
    color: #999;
  }

  .info-hint {
    background: none;
    border: 1px solid #a66969;
    color: #f5a7a7;
  }

    .kl-2-tabs {
      .kl-2-tabs__left, .kl-2-tabs__right {
        border: 1px solid rgba(113, 112, 112, 0.3);
        color: #ccc;
        border-bottom: none;
        &:hover {
          background: #444;
        }
      }

    .kl-2-tabs__left {
      border-top-left-radius: 10px;
      border-right: none;
    }

    .kl-2-tabs__right {
      border-top-right-radius: 10px;
      border-left: none;
    }

    .kl-2-tabs--active {
      color: #fff;
      background: #444;
      border: 1px solid var(--active-highlight-color);
      border-bottom: none;

      &:hover {
        background: #fff;
      }
    }
  }

  .popup-x {
    filter: invert(1);
  }

  .popup-x:hover {
    filter: invert(1) brightness(1.4);
  }

  .kl-preview-wrapper {
    background: rgb(33, 33, 33);
    box-shadow: inset 0 1px #636363, inset 0 -1px #636363;

    .kl-preview-wrapper__canvas {
      box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
    }
  }

  .kl-edit-crop-preview {
    border-top: 1px solid #636363;
    border-bottom: 1px solid #636363;
  }

  .kl-transparent-preview {
    box-shadow: inset 0 1px #636363, inset 0 -1px #636363;

    .kl-transparent-preview__canvas {
      box-shadow: 0 0 0 1px #777;
    }
  }

  .kl-merge-preview {
    box-shadow: 0 0 0 1px #636363;
  }

  a:link, a:not([href]), a:visited {
    color: #64acff;

    &:hover {
      color: #b2e8ff;
    }
  }

  .kl-color-picker__h, .kl-color-picker__sv, .kl-color-picker__preview {
    box-shadow: 0 0 0 1px rgb(100, 100, 100);
  }

  .kl-color-picker--active {
    box-shadow: 0 0 0 1px #999;
  }

  .kl-color-picker__secondary {
    box-shadow: 0 0 0 1px #7a7a7a;
  }

    .kl-stabilizer {
      color: #ccc;
    }

  .brush-alpha, .brush-alpha--selected {
    filter: invert(1) hue-rotate(180deg);
  }

    .kl-layer-preview {
      color: #ccc;
      canvas {
        box-shadow: 0 0 0 1px #7a7a7a;
      }
    }

  .kl-import-note, .kl-toolspace-note {
    background: none;
    color: #d5d558;
    border: 1px solid #d5d558;
  }

  .kl-curves-graph {
    canvas {
      background: #dedede;
      filter: invert(1) hue-rotate(180deg);
    }
  }

  .kl-curves-graph__grip {
    background: #fff;
    box-shadow: none;
  }

  .kl-storage-preview {
    background: #212121;
    box-shadow: inset 0 0 0 1px #5e5e5e;
    color: #aaa;
  }

  .kl-storage-preview__im {
    box-shadow: 0 0 0 1px #5e5e5e;
  }

  .kl-storage-about {
    box-shadow: inset 0 0 0 1px #ffff;
  }

    .kl-layer {
      background: transparent;
      border: 1px solid #4f4f4f;
      .kl-layer__label, .kl-layer__opacity-label {
        color: #ccc;
      }
    }

  .kl-layer--selected {
    background: #505050;
    border: 1px solid var(--active-highlight-color);

    .kl-layer__label {
      color: #fff;
    }
  }

  .kl-point-slider__line {
    background: #777;
  }

  .kl-point-slider__point {
    box-shadow: none;
  }

  .kl-overlay-toolspace {
    background: transparent;
    border: none;
    box-shadow: none;
  }

  .kl-overlay-toolspace__color {
    box-shadow: 0 0 0 1px #6e6e6e;
  }

  .image-toggle-active {
    background: rgba(255, 255, 255, 0.07);
  }

  .kl-file-save-row {
    box-shadow: inset 0 0 0 1px #fff3;
  }

  .grid-hr {
    border-bottom: 1px solid #fff2;
    border-top: none;
  }

  .kl-color-option {
    box-shadow: 0 0 0 1px #7a7a7a;
  }

  .kl-color-option--active {
    box-shadow: 0 0 0 2px var(--active-highlight-color), 0 0 5px 0 var(--active-highlight-color);
  }

  .kl-text-error {
    color: #ff3838;
  }

}


code:
/* --- entrypoint for embed ---*/
@import "style";

:root {
  --kl-color-hover: #0946e9;
}

body {
  margin: 0;
  font-family: Arial, sans-serif;
}

.toolspace-row-button__submit {
  background: var(--kl-color);
  color: #fff;
}

.toolspace-row-button-hover > .toolspace-row-button__submit {
  background: var(--kl-color-hover);
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.upload-overlay {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  background: rgba(50, 50, 50, 0.5);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 30px;
  padding-bottom: 10%;
}

.upload-overlay > * {
  filter: drop-shadow(0 0 8px rgba(0,0,0,1));
}

.spinner {
  width: 20px;
  height: 20px;
  border-left: 2px solid #fff;
  border-radius: 20px;
  margin-right: 10px;
  animation: spin 0.6s linear 0s infinite;
}

.loading-screen-error .spinner {
  display: none;
}
.loading-screen-error #loading-screen-text {
  background: black;
  padding-right: 5px;
}

// dark
body.kl-theme-dark {
  .upload-overlay {
    background: #212121a1;
  }
}