From f50ccaaf101a18c8f7f6d4f7e10d8a3016562c2e Mon Sep 17 00:00:00 2001 From: Send_Nukez Date: Mon, 15 Nov 2021 04:47:54 +0100 Subject: [PATCH] improve settings ui --- CHANGELOG.md | 1 + src/js/ConfigMenu.js | 85 ++++++++++++++++++++++++++++---------- src/styles/Colors.scss | 7 +++- src/styles/ConfigMenu.scss | 70 ++++++++++++++++++++++++------- 4 files changed, 123 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a32e6f..86d311c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Fixed: - Playing icon position being wrong when listening to a playlist that is inside a folder ([#106 (comment)](https://github.com/JulienMaille/dribbblish-dynamic-theme/issues/106#issuecomment-967208507)) Improved: +- The settings UI now better represents grouped items - Settings that have been changed from default will now show a line next to them. Inspired by the [Visual Studio Code settings UI](https://d33wubrfki0l68.cloudfront.net/d1f1ea4def506997ced23d3d912154794e530e1c/063d2/assets/img/blog/2020-09-17-vscode-settings/settings-ui.png) - Checkbox / Switch input styles are now more in line with other input styles - Available updates are now shown as a clickable button next to your user icon instead of having to open the user menu diff --git a/src/js/ConfigMenu.js b/src/js/ConfigMenu.js index d623bae..3a999fe 100644 --- a/src/js/ConfigMenu.js +++ b/src/js/ConfigMenu.js @@ -20,6 +20,7 @@ export default class ConfigMenu { * @property {Boolean} [insertOnTop=false] * @property {Boolean} [fireInitialChange=true] * @property {Boolean} [save=true] + * @property {validate} [validate] * @property {showChildren} [showChildren] * @property {onAppended} [onAppended] * @property {onChange} [onChange] @@ -34,6 +35,13 @@ export default class ConfigMenu { * @property {Boolean} [toggleable=true] */ + /** + * @callback validate + * @this {DribbblishConfigItem} + * @param {any} value + * @returns {Boolean | String[]} + */ + /** * @callback showChildren * @this {DribbblishConfigItem} @@ -116,6 +124,7 @@ export default class ConfigMenu { elem.setAttribute("type", options.type); if (options.hidden) elem.setAttribute("hidden", true); if (options.childOf) elem.setAttribute("parent", options.childOf); + if (options.children.length > 0) elem.setAttribute("children", options.children.map((c) => c.key).join(" ")); elem.innerHTML = /* html */ ` ${ options.name != null && options.description != null @@ -177,6 +186,7 @@ export default class ConfigMenu { insertOnTop: false, fireInitialChange: true, save: true, + validate: () => true, showChildren: () => true, onAppended: () => {}, onChange: () => {}, @@ -205,6 +215,18 @@ export default class ConfigMenu { this.#config[options.key] = options; + function validate(val) { + const isValid = options.validate.call(options, val); + $(`.dribbblish-config-item[key="${options.key}"]`).attr("invalid", !isValid ? "" : null); + return isValid; + } + + const change = (val) => { + if (!validate(val)) return; + this.set(options.key, val, options.save); + options.onChange(val); + }; + if (options.type == "checkbox") { const input = /* html */ ` @@ -215,8 +237,7 @@ export default class ConfigMenu { this.#addInputHTML({ ...options, input }); $(`#dribbblish-config-input-${options.key}`).on("change", (e) => { - this.set(options.key, e.target.checked, options.save); - options.onChange(this.get(options.key)); + change(e.target.checked); }); } else if (options.type == "select") { // Validate @@ -233,8 +254,7 @@ export default class ConfigMenu { this.#addInputHTML({ ...options, input }); $(`#dribbblish-config-input-${options.key}`).on("change", (e) => { - this.set(options.key, e.target.value, options.save); - options.onChange(this.get(options.key)); + change(e.target.value); }); } else if (options.type == "button") { if (typeof options.data != "string") options.data = options.name; @@ -257,9 +277,9 @@ export default class ConfigMenu { } else if (options.type == "number") { // Validate if (options.defaultValue == null) options.defaultValue = 0; - const val = this.get(options.key); - if (options.data.min != null && val < options.data.min) this.set(options.key, options.data.min, options.save); - if (options.data.max != null && val > options.data.max) this.set(options.key, options.data.max, options.save); + const _val = this.get(options.key); + if (options.data.min != null && _val < options.data.min) this.set(options.key, options.data.min, options.save); + if (options.data.max != null && _val > options.data.max) this.set(options.key, options.data.max, options.save); const input = /* html */ ` options.data.max) e.target.value = options.data.max; - this.set(options.key, Number(e.target.value), options.save); - options.onChange(this.get(options.key)); + change(Number(e.target.value)); }); } else if (options.type == "text") { if (options.defaultValue == null) options.defaultValue = ""; @@ -294,9 +313,10 @@ export default class ConfigMenu { this.#addInputHTML({ ...options, input }); $(`#dribbblish-config-input-${options.key}`).on("input", (e) => { - // TODO: maybe add an validation function via `data.validate` - this.set(options.key, e.target.value, options.save); - options.onChange(this.get(options.key)); + const val = e.target.value; + if (!validate(val)) return; + this.set(options.key, val, options.save); + options.onChange(val); }); } else if (options.type == "textarea") { if (options.defaultValue == null) options.defaultValue = ""; @@ -307,9 +327,7 @@ export default class ConfigMenu { this.#addInputHTML({ ...options, input }); $(`#dribbblish-config-input-${options.key}`).on("input", (e) => { - // TODO: maybe add an validation function via `data.validate` - this.set(options.key, e.target.value, options.save); - options.onChange(this.get(options.key)); + change(e.target.value); }); } else if (options.type == "slider") { // Validate @@ -336,8 +354,7 @@ export default class ConfigMenu { $(`#dribbblish-config-input-${options.key}`).attr("tooltip", `${e.target.value}${options.data?.suffix ?? ""}`); $(`#dribbblish-config-input-${options.key}`).attr("value", e.target.value); - this.set(options.key, Number(e.target.value), options.save); - options.onChange(this.get(options.key)); + change(Number(e.target.value)); }); } else if (options.type == "time") { // Validate @@ -350,8 +367,7 @@ export default class ConfigMenu { $(`#dribbblish-config-input-${options.key}`).on("input", (e) => { $(`#dribbblish-config-input-${options.key}`).attr("value", e.target.value); - this.set(options.key, e.target.value, options.save); - options.onChange(this.get(options.key)); + change(e.target.value); }); } else if (options.type == "color") { // Validate @@ -362,8 +378,7 @@ export default class ConfigMenu { this.#addInputHTML({ ...options, input }); $(`#dribbblish-config-input-${options.key}`).on("input", (e) => { - this.set(options.key, e.target.value, options.save); - options.onChange(this.get(options.key)); + change(e.target.value); }); } else { throw new Error(`Config Type "${options.type}" invalid`); @@ -372,6 +387,7 @@ export default class ConfigMenu { // Re-write internal config since some values may have changed this.#config[options.key] = options; + validate(this.get(options.key)); $(`.dribbblish-config-item[key="${options.key}"]`).attr("changed", options.save && this.get(options.key) != options.defaultValue ? "" : null); options.children.forEach((child) => this.register(child)); @@ -466,7 +482,32 @@ export default class ConfigMenu { */ #setHidden(key, hidden) { this.#config[key].hidden = hidden; - $(`.dribbblish-config-item[key="${key}"]`).attr("hidden", hidden ? "" : null); + const $elem = $(`.dribbblish-config-item[key="${key}"]`); + $elem.attr("hidden", hidden ? "" : null); + + // If element has children or a parent + if ($elem.attr("parent") != null || $elem.attr("children") != null) { + // Get parent of element block + const $parent = $elem.attr("parent") != null ? $(`[children~="${key}"]`) : $elem; + const $nextChildren = $parent.nextAll(`[parent="${$parent.attr("key")}"]`); + + // Make parent connect on bottom when children are visible + $parent.attr("connect-bottom", $nextChildren.filter(":not([hidden])").length > 0 ? "" : null); + + // Reset all children's bottom connection + $nextChildren.each(function () { + $(this).attr("connect-bottom", null); + }); + // Add bottom connection to all but the last visible child + $nextChildren + .filter(":not([hidden])") + .slice(0, -1) + .each(function () { + $(this).attr("connect-bottom", ""); + }); + + //* NOTE: All children automatically have a top connection + } } getOptions(key) { diff --git a/src/styles/Colors.scss b/src/styles/Colors.scss index 4be09b6..138f53e 100644 --- a/src/styles/Colors.scss +++ b/src/styles/Colors.scss @@ -46,10 +46,13 @@ $props-to-transition: ("sidebar", "main", "text", "button"); } // Color Function -@function spiceColor($key, $alpha: 1) { +// $light-offset is added when in light mode +@function spiceColor($key, $alpha: 1, $light-offset: 0) { @if $alpha == 1 { @return var(--spice-#{$key}); - } @else { + } @else if $light-offset == 0 { @return rgba(var(--spice-rgb-#{$key}), $alpha); + } @else { + @return rgba(var(--spice-rgb-#{$key}), calc($alpha + var(--is_light) * $light-offset)); } } diff --git a/src/styles/ConfigMenu.scss b/src/styles/ConfigMenu.scss index c0a90a2..6a2c8e5 100644 --- a/src/styles/ConfigMenu.scss +++ b/src/styles/ConfigMenu.scss @@ -23,7 +23,7 @@ z-index: 1; position: relative; width: clamp(500px, 50%, 650px); - background-color: spiceColor("main", 0.9); + background-color: spiceColor("main", 0.95); backdrop-filter: blur(3px); padding: 20px 15px; border-radius: var(--main-corner-radius); @@ -44,22 +44,23 @@ display: flex; width: 100%; flex-direction: column; - gap: 16px; + gap: 8px; max-height: 60vh; overflow-y: auto; - padding: 0px 50px; + padding: 0px 25px; .dribbblish-config-area { display: flex; flex-direction: column; - gap: 16px; + align-items: center; + gap: 8px; &[collapsed] { overflow: hidden; min-height: 38px; //for some reason height alone isn't enough height: 38px; - > h2 svg { + .dribbblish-config-area-header svg { transform: rotate(270deg); } } @@ -71,6 +72,7 @@ .dribbblish-config-area-header { position: relative; text-align: center; + width: fit-content; height: 38px; display: flex; gap: 10px; @@ -89,7 +91,8 @@ .dribbblish-config-area-items { display: flex; flex-direction: column; - gap: 16px; + gap: 8px; + width: 100%; .dribbblish-config-item { position: relative; @@ -99,23 +102,58 @@ justify-content: space-between; align-items: center; gap: 10px; + padding: 8px 16px; &[hidden] { display: none; } - &[parent] { - padding-left: 16px; - } - - &[changed]::before { + &::before { + z-index: -1; content: ""; position: absolute; - left: -13px; - top: -3px; - bottom: -3px; - width: 3px; - background-color: spiceColor("sidebar"); + inset: 0px; + border-radius: var(--main-corner-radius); + background-color: spiceColor("subtext", 0.03, 0.04); + } + + &[parent]::before { + top: -8px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + } + + &[connect-bottom]::before { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + } + + &[invalid]::before { + background-color: rgba(red, 0.2); + } + + &[changed] { + &::after { + content: ""; + position: absolute; + left: 0px; + top: 0px; + bottom: 0px; + width: 5px; + background-color: spiceColor("text"); + border-top-left-radius: var(--main-corner-radius); + border-bottom-left-radius: var(--main-corner-radius); + } + + &[parent]::after { + top: -4px; + border-top-left-radius: 0px; + } + + &[connect-bottom]::after { + bottom: -4px; + border-bottom-left-radius: 0px; + } } .dribbblish-config-item-header {