<script>
    import { onMount, onDestroy, tick } from "svelte";

    import "../assets/index.scss";
    import { pick, debounce, pascalCaseToSentence, capitalizeFirstLetter, nbChars } from "../util/util";
    import { computeContours } from "../util/boxesContour";
    import ColorPicker from "./ColorPicker.svelte";
    import { getFonts } from "../util/fonts";

    const strokeElements = [
        "altGlyph",
        "circle",
        "ellipse",
        "line",
        "path",
        "polygon",
        "polyline",
        "rect",
        "text",
        "textPath",
        "tref",
        "tspan",
        "g",
    ];

    const borderProps = ["border-radius", "border-width", "border-color", "border-style"];
    const backgroundProps = ["background-color"];
    const fontProps = ["font-family", "font-size", "font-weight", "color"];
    const pathProps = ["stroke-width", "stroke", "stroke-dasharray", "stroke-linejoin", "fill"];
    const cssPropByType = {
        "border-radius": { type: "slider", min: 0, max: 30, suffix: "px" },
        "border-width": { type: "slider", min: 0, max: 30, suffix: "px" },
        "border-style": {
            type: "select",
            choices: () => ["none", "dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset"],
        },
        "border-color": { type: "color" },
        "font-family": { type: "select", choices: getFontFamilies },
        "font-size": { type: "slider", min: 0, max: 40, suffix: "px" },
        "font-weight": { type: "slider", min: 0, max: 800 },
        color: { type: "color" },
        "stroke-width": {
            type: "slider",
            min: 0,
            max: 5,
            step: 0.5,
            suffix: "px",
        },
        stroke: { type: "color" },
        "stroke-linejoin": {
            type: "select",
            choices: () => ["bevel", "miter", "round"],
        },
        fill: { type: "color" },
        "stroke-dasharray": { type: "slider", min: 0, max: 30, suffix: "px" },
        "background-color": { type: "color" },
    };

    const propertyIcons = {
        stroke: `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="2" y1="14" x2="14" y2="2"/></svg>`,
        fill: `<svg viewBox="0 0 16 16" fill="currentColor"><rect x="2" y="2" width="12" height="12" rx="2"/></svg>`,
        color: `<svg viewBox="0 0 16 16" fill="currentColor"><text x="3" y="11" font-size="10" font-weight="bold">A</text><rect x="2" y="13" width="12" height="2"/></svg>`,
        "border-color": `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="12" height="12" rx="1"/></svg>`,
        "background-color": `<svg viewBox="0 0 16 16" fill="currentColor"><rect x="4" y="4" width="10" height="10" rx="1" opacity="0.5"/><rect x="2" y="2" width="10" height="10" rx="1"/></svg>`,
        "border-radius": `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 10 L2 2 L10 2" stroke-linecap="round"/><path d="M2 10 Q2 14 6 14 L14 14" stroke-linecap="round"/></svg>`,
        "border-width": `<svg viewBox="0 0 16 16" fill="currentColor"><rect x="2" y="2" width="12" height="2"/><rect x="2" y="7" width="12" height="3"/><rect x="2" y="13" width="12" height="1"/></svg>`,
        "border-style": `<svg viewBox="0 0 16 16" fill="currentColor"><rect x="2" y="2" width="12" height="1.5"/><rect x="2" y="7" width="3" height="1.5"/><rect x="7" y="7" width="3" height="1.5"/><rect x="2" y="12" width="1.5" height="1.5"/><rect x="5" y="12" width="1.5" height="1.5"/><rect x="8" y="12" width="1.5" height="1.5"/><rect x="11" y="12" width="1.5" height="1.5"/></svg>`,
        "font-size": `<svg viewBox="0 0 16 16" fill="currentColor"><text x="1" y="12" font-size="12" font-weight="bold">T</text><text x="9" y="12" font-size="8" font-weight="bold">T</text></svg>`,
        "font-weight": `<svg viewBox="0 0 16 16" fill="currentColor"><text x="3" y="13" font-size="14" font-weight="bold">B</text></svg>`,
        "font-family": `<svg viewBox="0 0 16 16" fill="currentColor"><text x="4" y="13" font-size="14" font-style="italic" font-family="serif">F</text></svg>`,
        "stroke-width": `<svg viewBox="0 0 16 16" fill="currentColor"><rect x="2" y="2" width="12" height="2"/><rect x="2" y="7" width="12" height="3"/><rect x="2" y="13" width="12" height="1"/></svg>`,
        "stroke-dasharray": `<svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="7" width="4" height="2"/><rect x="7" y="7" width="4" height="2"/><rect x="13" y="7" width="2" height="2"/></svg>`,
        "stroke-linejoin": `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><polyline points="3,12 8,4 13,12"/></svg>`,
    };

    // Props
    const props = $props();
    const getElems = props.getElems ?? null;
    const listenOnClick = props.listenOnClick ?? false;
    const onStyleChanged = props.onStyleChanged ?? (() => {});
    const customProps = props.customProps ?? {};
    const inlineDeletable = props.inlineDeletable ?? (() => true);
    const cssRuleFilter = props.cssRuleFilter ?? null;
    const getCssRuleName =
        props.getCssRuleName ??
        ((cssRuleName, element) => {
            if (cssRuleName === "inline") return "Selected element";
            return cssRuleName;
        });
    const ignoredProps = props.ignoredProps ?? [];

    const typeText = "text";
    const typeBorder = "border";
    const typeStroke = "stroke";
    const typeBackground = "background";
    const customType = "custom";
    const propByType = {
        [typeText]: fontProps,
        [typeBorder]: borderProps,
        [typeStroke]: pathProps,
        [typeBackground]: backgroundProps,
        [customType]: Object.keys(customProps),
    };
    const inputTypeOrder = { slider: 0, select: 1, color: 2 };

    // State variables
    let elementToListen = $state(null);
    let clickedElement = $state(null);
    let positionAnchor = $state();
    let self = $state();
    let helperElemWrapper = $state();
    let pathWithHoles = $state("");
    let pageDimensions = $state({ width: 0, height: 0 });
    let targetsToSearch = $state([[]]);
    let allRules = $state([]); // list of list of CSS rules, for every target element
    let allTypes = $state([]); // list of list of types (e.g color, border), for every target element
    let selectedElemIndex = $state(0);
    let selectedRuleIndex = $state(0);
    let selectedTypeIndex = $state(0);
    let propsByType = $state(); // propType -> {[props], selected}
    let allCurrentPropDefs = $state({}); // propName => selectorDef
    let bringableToFront = $state([]); // null = not bringable, true = bringable, false = was bringed
    let hasDisplayedCustom = $state(false);
    let baseUrl = $state(window.location.href.replace(/#.*$/, ""));

    // Reactive derived values
    const currentElement = $derived(targetsToSearch[selectedElemIndex]?.[0]);
    const currentRule = $derived(allRules[selectedElemIndex]?.[selectedRuleIndex]);
    let curType = $state();

    // Effects
    $effect(() => {
        if (elementToListen !== null) {
            init();
        }
    });

    $effect(() => {
        if (allTypes[selectedElemIndex]?.[selectedTypeIndex] !== curType) {
            curType = allTypes[selectedElemIndex]?.[selectedTypeIndex];
        }
    });

    $effect(() => {
        if (curType || selectedRuleIndex || selectedElemIndex) {
            initAndGroup();
        }
    });

    onMount(() => {
        close();
        elementToListen = self.parentNode;
        document.body.appendChild(self);
        document.body.appendChild(helperElemWrapper);
        document.body.appendChild(positionAnchor);
        udpatePageDimensions();
        // make sure the layout is computed to get the client size
        setTimeout(() => {
            udpatePageDimensions();
        }, 1000);
        window.addEventListener("resize", udpatePageDimensions);
    });

    onDestroy(() => {
        window.removeEventListener("resize", udpatePageDimensions);
        if (listenOnClick) elementToListen.removeEventListener("click", _open);
    });

    function getFontFamilies() {
        return getFonts();
    }

    function initAndGroup() {
        const allProps = { ...cssPropByType, ...customProps };
        const _allCurrentPropDefs = pick(allProps, propByType[curType]);
        ignoredProps.forEach((prop) => delete _allCurrentPropDefs[prop]);
        const elemTagName = currentElement?.tagName?.toLowerCase();
        if (curType === typeText && (elemTagName === "text" || elemTagName === "tspan")) {
            delete _allCurrentPropDefs["color"];
        }
        Object.keys(_allCurrentPropDefs).forEach((key) => {
            const propSelectType = _allCurrentPropDefs[key].type;
            let retrieveType = "number";
            if (propSelectType === "color") retrieveType = "rgb";
            else if (propSelectType === "select") retrieveType = "raw";
            if (_allCurrentPropDefs[key].getter) {
                const val = _allCurrentPropDefs[key].getter(currentElement);
                if (val === null) {
                    delete _allCurrentPropDefs[key];
                    return;
                }
                _allCurrentPropDefs[key].value = val;
                _allCurrentPropDefs[key].displayed = val;
            } else {
                _allCurrentPropDefs[key].displayed = getComputedPropValue(currentElement, key, "raw");
                _allCurrentPropDefs[key].value = getComputedPropValue(currentElement, key, retrieveType);
                // Special handling for SVG fill with url() expressions (e.g., gradients)
                if (key === "fill") {
                    const rawFill = currentElement.getAttribute?.("fill") || _allCurrentPropDefs[key].displayed;
                    if (rawFill && rawFill.includes("url(")) {
                        _allCurrentPropDefs[key].originalUrl = rawFill;
                        _allCurrentPropDefs[key].value = "#00000000";
                    }
                }
            }
        });
        const _propsByType = Object.entries(_allCurrentPropDefs)
            .reduce((byType, [propName, selectorDef]) => {
                const selectorType = selectorDef.type;
                const existing = byType.find((x) => x.type === selectorType);
                if (!existing)
                    byType.push({
                        selected: 0,
                        props: [propName],
                        type: selectorType,
                    });
                else existing.props.push(propName);
                return byType;
            }, [])
            .sort((a, b) => {
                if (inputTypeOrder[a.type] < inputTypeOrder[b.type]) return -1;
                if (inputTypeOrder[a.type] > inputTypeOrder[b.type]) return 1;
                return 0;
            });

        // Pre-select fill over stroke if the element has a non-transparent fill
        const colorGroup = _propsByType.find((g) => g.type === "color");
        if (colorGroup) {
            const fillIndex = colorGroup.props.indexOf("fill");
            const fillValue = _allCurrentPropDefs["fill"]?.value;
            if (fillIndex !== -1 && fillValue && fillValue !== "#00000000") {
                colorGroup.selected = fillIndex;
            }
        }

        propsByType = _propsByType;
        allCurrentPropDefs = _allCurrentPropDefs;
        updateHelpers();
    }

    function getRuleNames(rules) {
        if (!rules) return [];
        return rules.map((rule, i) => {
            if (rule === "inline") return "inline";
            const cssSelector = rule.selectorText;
            const title = rule.parentStyleSheet.title || `${i}`;
            return `${title}: ${cssSelector}`;
        });
    }

    function getRuleNamesTransformed(rules) {
        return getRuleNames(rules).map((name) => getCssRuleName(name, clickedElement));
    }

    let warningDisplayed = new Set();
    function getMatchedCSSRules(elems) {
        const sheets = document.styleSheets;
        return elems.reduce((matchedRulesByElem, elemDef) => {
            const el = elemDef[0];
            const matchedRules = [];
            if (cssRuleFilter === null || cssRuleFilter(el, "inline")) {
                matchedRules.push("inline");
            }
            for (let i in sheets) {
                try {
                    const rules = sheets[i].cssRules;
                    for (let r in rules) {
                        let selectorText = rules[r].selectorText;
                        if (!selectorText || rules[r].selectorText.length > 50) continue; // skip selectors too long
                        if (selectorText.split(",").some((selector) => selector === "*")) continue; // skip * selector
                        if (selectorText.endsWith(":hover"))
                            selectorText = selectorText.substring(0, selectorText.length - ":hover".length);
                        if (el.matches(selectorText)) {
                            if (cssRuleFilter !== null && !cssRuleFilter(el, rules[r].selectorText)) continue;
                            matchedRules.push(rules[r]);
                        }
                    }
                } catch (err) {
                    if (!warningDisplayed.has(i)) {
                        console.warn(
                            "Style editor: Not able to access",
                            sheets[i].ownerNode,
                            "sheet. Try CORS loading the sheet if you want to edit it.",
                        );
                        warningDisplayed.add(i);
                    }
                }
            }
            matchedRulesByElem.push(matchedRules);
            return matchedRulesByElem;
        }, []);
    }

    function getEditableTypes(elems) {
        return elems.reduce((typesByElem, elemDef) => {
            const elem = elemDef[0];
            const types = [];
            if (
                elem.firstChild &&
                ((!elem.firstElementChild && elem.firstChild.nodeType === 3) || elem.firstChild.tagName === "tspan")
            ) {
                // Node.TEXT_NODE
                types.push(typeText);
            }
            const elemTagName = elem.tagName.toLowerCase();
            let bringable = false;
            if (strokeElements.includes(elemTagName)) {
                types.push(typeStroke);
                const parentTag = elem.parentElement.tagName.toLowerCase();
                if (
                    parentTag === "g" &&
                    elem.previousElementSibling &&
                    elem.previousElementSibling.tagName.toLowerCase() == elemTagName
                ) {
                    bringable = true;
                }
            } else {
                types.push(typeBorder);
                types.push(typeBackground);
            }
            if (bringable) bringableToFront.push(true);
            else bringableToFront.push(null);
            typesByElem.push(types);
            return typesByElem;
        }, []);
    }

    function init() {
        if (listenOnClick) elementToListen.addEventListener("click", _open);
    }

    function _open(e) {
        open(e.target, e.pageX, e.pageY);
    }

    export async function open(el, x, y) {
        clickedElement = el;
        baseUrl = window.location.href.replace(/#.*$/, "");
        udpatePageDimensions();
        if (el.classList.contains("overlay-over")) return overlayClicked();
        else if (self.contains(el)) return;
        selectedElemIndex = 0;
        selectedRuleIndex = 0;
        selectedTypeIndex = 0;
        bringableToFront = [];
        allTypes = [];
        allRules = [];
        if (getElems) targetsToSearch = getElems(el);
        else targetsToSearch = [[el, "Clicked"]];
        allTypes = getEditableTypes(targetsToSearch);
        hasDisplayedCustom = false;
        allRules = getMatchedCSSRules(targetsToSearch);
        if (allRules.every((rules) => rules.length === 0)) return;
        for (let def of Object.values(customProps)) {
            if (def.getter(el) !== null) {
                hasDisplayedCustom = true;
                break;
            }
        }
        if (Object.keys(customProps).length) {
            allTypes[0].push(customType);
        }
        await tick();
        initAndGroup();
        if (x && y) show(x, y);
        else {
            const rect = getBoundingBoxInfos(el, 15);
            show(rect.left, rect.top);
        }
    }

    export function close() {
        self.style.display = "none";
        helperElemWrapper.style.display = "none";
        pathWithHoles = "";
    }

    export function isOpened() {
        return self.style.display === "block";
    }

    function overlayClicked() {
        close();
    }

    function show(x, y) {
        self.style.display = "block";
        self.style.opacity = 0;
        const popupDimension = self.getBoundingClientRect();
        x = x + popupDimension.width + 20 > pageDimensions.width ? x - popupDimension.width - 20 : x + 20;
        y = y + popupDimension.height + 20 > pageDimensions.height ? y - popupDimension.height - 20 : y + 20;
        y = Math.max(y, 0);
        self.style.left = x + "px";
        self.style.top = y + "px";
        helperElemWrapper.style.display = "block";
        self.style.opacity = 1;
        updateHelpers();
    }

    async function updateHelpers() {
        await tick();
        if (!currentRule) return;
        let matching;
        if (currentRule === "inline") matching = [currentElement];
        else {
            const selector = currentRule.selectorText.replace(/(:hover)|:focus|\.hovered/g, "");
            matching = Array.from(document.querySelectorAll(selector));
        }
        const boundingBoxes = matching.map((el) => getBoundingBoxInfos(el, 10));
        pathWithHoles = computeContours(boundingBoxes, pageDimensions);
    }

    function getBoundingBoxInfos(el, padding = 0) {
        const rect = el.getBoundingClientRect();
        return {
            left: rect.left + window.scrollX - padding,
            top: rect.top + window.scrollY - padding,
            width: rect.width + padding * 2,
            height: rect.height + padding * 2,
            right: rect.left + window.scrollX + rect.width + padding,
            bottom: rect.top + window.scrollY + rect.height + padding,
        };
    }

    function _updateProp(propName, val, suffix) {
        const finalValue = suffix ? val + suffix : val;
        if (currentRule === "inline") {
            if (allCurrentPropDefs[propName].setter) {
                allCurrentPropDefs[propName].setter(currentElement, val);
            } else {
                const style = currentElement.style; // do not trigger reactivity on currentElement
                style[propName] = finalValue;
            }
        } else currentRule.style.setProperty(propName, finalValue);
        allCurrentPropDefs[propName].value = val;
        allCurrentPropDefs[propName].displayed = finalValue;

        onStyleChanged(currentElement, currentRule, propName, finalValue);
        updateHelpers();
    }
    const updateProp = debounce(_updateProp, 100);

    function udpatePageDimensions() {
        const bodyStyle = getComputedStyle(document.body);
        const marginLeft = parseInt(bodyStyle.marginLeft);
        const marginRight = parseInt(bodyStyle.marginRight);
        const marginTop = parseInt(bodyStyle.marginTop);
        const marginBottom = parseInt(bodyStyle.marginBottom);
        pageDimensions = {
            width: document.body.offsetWidth + marginLeft + marginRight,
            height: document.body.offsetHeight + marginTop + marginBottom,
        };
    }

    function cssRgbToHex(rgbStr) {
        const m = rgbStr.match(/[0-9\.]+/g).map((i) => parseFloat(i));
        if (m.length === 3) m.push(1);
        return m.reduce((hexStr, cur, i) => {
            if (i === 3)
                hexStr += Math.round(cur * 255)
                    .toString(16)
                    .padStart(2, "0");
            else hexStr += cur.toString(16).padStart(2, "0");
            return hexStr;
        }, "#");
    }

    // type one of: "number", "rgb", "font", "raw"
    function parsePropvalue(value, type = "number") {
        if (type == "raw") return value;
        if (type == "number" && /[0-9]+(px)|(em)|(rem)/.test(value)) return parseInt(value);
        if (type == "rgb") {
            if (value === "none") return "#00000000";
            if (value.includes("rgb") || value[0] == "#") return cssRgbToHex(value);
        }
        return value;
    }

    function getComputedPropValue(el, cssProp, type = "number") {
        let currentStyleValue = currentRule?.style?.[cssProp];
        if (!currentStyleValue) {
            const computed = getComputedStyle(el);
            currentStyleValue = computed[cssProp];
        }
        return parsePropvalue(currentStyleValue, type);
    }

    function bringToFront() {
        bringableToFront[selectedElemIndex] = false;
        currentElement.parentNode.appendChild(currentElement);
        onStyleChanged(currentElement, currentRule, "bringtofront", null);
    }

    function deleteElem() {
        currentElement.remove();
        close();
    }
    function deleteProp(propName) {
        const propDef = allCurrentPropDefs[propName];
        const isCustomProp = propName in customProps;

        // Check if custom prop has a defaultValue
        if (isCustomProp && propDef && "defaultValue" in customProps[propName]) {
            const defaultValue = customProps[propName].defaultValue;
            if (propDef.setter) {
                propDef.setter(currentElement, defaultValue);
            }
            propDef.value = defaultValue;
            propDef.displayed = defaultValue;
            onStyleChanged(currentElement, currentRule, propName, defaultValue);
        } else if (propName === "fill" && propDef?.originalUrl) {
            // Restore original url() expression for SVG fill
            const urlValue = propDef.originalUrl;
            if (currentRule === "inline") {
                currentElement.style.fill = urlValue;
            } else {
                currentRule.style.setProperty("fill", urlValue);
            }
            propDef.value = "#00000000";
            propDef.displayed = urlValue;
            onStyleChanged(currentElement, currentRule, "fill", urlValue);
        } else {
            // Standard CSS property reset
            if (currentRule === "inline") {
                currentElement.style.removeProperty(propName);
            } else {
                currentRule.style.removeProperty(propName);
            }
            onStyleChanged(currentElement, currentRule, propName, null);
            // Update the displayed value without rebuilding (which would reset selection)
            if (propDef) {
                const propSelectType = propDef.type;
                let retrieveType = "number";
                if (propSelectType === "color") retrieveType = "rgb";
                else if (propSelectType === "select") retrieveType = "raw";
                propDef.displayed = getComputedPropValue(currentElement, propName, "raw");
                propDef.value = getComputedPropValue(currentElement, propName, retrieveType);
            }
        }
        updateHelpers();
    }

    function selectRule(ruleIndex) {
        const newRule = allRules[selectedElemIndex]?.[ruleIndex];
        if (newRule !== "inline" && selectedTypeIndex === allTypes[selectedElemIndex].length - 1) {
            selectedTypeIndex = 0;
        }
        selectedRuleIndex = ruleIndex;
    }
</script>

<div bind:this={positionAnchor} style="position: absolute;"></div>
<svg
    bind:this={helperElemWrapper}
    class="ise-helper-wrapper"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    width={pageDimensions.width}
    height={pageDimensions.height}
    onclick={overlayClicked}
>
    <clipPath id="overlay-clip" clip-rule="evenodd">
        <path d={pathWithHoles} />
    </clipPath>
    <rect y="0" x="0" height="100%" width="100%" class="overlay-over" style="clip-path: url({baseUrl}#overlay-clip)" />
</svg>

<div class="ise" bind:this={self}>
    <button class="close-button" title="Close" onclick={close}>
        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5">
            <path d="M4 4 L12 12 M12 4 L4 12" />
        </svg>
    </button>
    {#if targetsToSearch.length > 1}
        <div class="select-tab">
            <b> Element </b>
            {#each targetsToSearch as [_, name], elemIndex}
                <span
                    class:selected={selectedElemIndex === elemIndex}
                    onclick={() => {
                        selectedElemIndex = elemIndex;
                        selectedRuleIndex = 0;
                    }}
                >
                    {name}
                </span>
            {/each}
        </div>
    {/if}
    {#if allRules[selectedElemIndex]?.length > 1}
        <div class="select-tab">
            <b> Applied to: </b>
            {#if nbChars(getRuleNamesTransformed(allRules[selectedElemIndex])) > 30}
                <select value={selectedRuleIndex} onchange={(e) => selectRule(+e.target.value)}>
                    {#each getRuleNames(allRules[selectedElemIndex]) as ruleName, ruleIndex}
                        <option value={ruleIndex}>{getCssRuleName(ruleName, clickedElement)}</option>
                    {/each}
                </select>
            {:else}
                {#each getRuleNames(allRules[selectedElemIndex]) as ruleName, ruleIndex}
                    <span
                        title={ruleName}
                        class:selected={selectedRuleIndex === ruleIndex}
                        onclick={() => {
                            selectRule(ruleIndex);
                        }}
                    >
                        {getCssRuleName(ruleName, clickedElement)}</span
                    >
                {/each}
            {/if}
        </div>
    {/if}
    {#if (allTypes[selectedElemIndex] || []).filter((t) => t !== "custom" || (currentRule === "inline" && hasDisplayedCustom)).length > 1}
        <div class="select-tab">
            <b> Property type: </b>
            {#each allTypes[selectedElemIndex] || [] as type, typeIndex}
                <!-- Only display "custom" on "inline" rule -->
                {#if type !== "custom" || (currentRule === "inline" && type === "custom" && hasDisplayedCustom)}
                    <span
                        class:selected={selectedTypeIndex === typeIndex}
                        onclick={() => {
                            selectedTypeIndex = typeIndex;
                        }}
                    >
                        {type === "stroke" ? "SVG paint" : capitalizeFirstLetter(type)}
                    </span>
                {/if}
            {/each}
        </div>
    {/if}
    {#if allTypes[selectedElemIndex]}
        <div class="editor">
            {#each propsByType as choices}
                {@const selectedName = choices.props[choices.selected]}
                <div class="prop-section {choices.type}">
                    <div class="prop-header">
                        <div class="prop-name">
                            {#if choices.props.length > 1}
                                <div class="icon-selector">
                                    {#each choices.props as propName, i}
                                        <button
                                            class="icon-btn"
                                            class:selected={i === choices.selected}
                                            title={choices.type === "color"
                                                ? `${capitalizeFirstLetter(propName)} color`
                                                : pascalCaseToSentence(propName)}
                                            onclick={async () => {
                                                choices.selected = i;
                                                await tick();
                                            }}
                                        >
                                            {@html propertyIcons[propName]}
                                        </button>
                                    {/each}
                                </div>
                                <span class="selected-label"
                                    >{choices.type === "color"
                                        ? `${capitalizeFirstLetter(selectedName)} color`
                                        : pascalCaseToSentence(selectedName)}</span
                                >
                            {:else}
                                <span>{pascalCaseToSentence(selectedName)}</span>
                            {/if}
                        </div>
                        <button class="delete-btn" title="Reset to default" onclick={() => deleteProp(selectedName)}>
                            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2">
                                <path d="M4 4 L12 12 M12 4 L4 12" />
                            </svg>
                        </button>
                    </div>
                    {#if choices.type === "slider"}
                        <input
                            type="range"
                            min={allCurrentPropDefs[selectedName].min}
                            max={allCurrentPropDefs[selectedName].max}
                            step={allCurrentPropDefs[selectedName].step || 1}
                            value={allCurrentPropDefs[selectedName].value}
                            onchange={(e) =>
                                updateProp(
                                    selectedName,
                                    e.target.value,
                                    allCurrentPropDefs[selectedName].suffix,
                                    e.target,
                                )}
                        />
                        <span class="current-value">
                            {allCurrentPropDefs[selectedName].displayed}
                        </span>
                    {:else if choices.type == "select"}
                        {@const choices = allCurrentPropDefs[selectedName].choices()}
                        <select onchange={(e) => updateProp(selectedName, e.target.value)}>
                            {#if !choices.includes(allCurrentPropDefs[selectedName].value)}
                                <option selected="true"> --- </option>
                            {/if}
                            {#each choices as choice}
                                <option selected={choice == allCurrentPropDefs[selectedName].value || null}>
                                    {choice}
                                </option>
                            {/each}
                        </select>
                    {:else if choices.type == "color"}
                        <ColorPicker
                            value={allCurrentPropDefs[selectedName].value}
                            onChange={(color) => updateProp(selectedName, color)}
                        />
                    {/if}
                </div>
            {/each}
            {#if currentRule === "inline" && bringableToFront[selectedElemIndex] !== null}
                <div class="btn" class:active={bringableToFront[selectedElemIndex] === true} onclick={bringToFront}>
                    Bring to front
                </div>
            {/if}
            {#if currentRule === "inline" && inlineDeletable(currentElement)}
                <div class="btn delete-elem" onclick={deleteElem}>Delete element</div>
            {/if}
        </div>
    {/if}
</div>
