improved settings layout and UX, added bake button, fixed icons, added icon colours, improved BoolBox appearance, added snowflake icon, removed unused BeatLikeLoopSettings, added disabled buttons

This commit is contained in:
Daniel Ledda
2022-03-13 22:28:17 +01:00
parent 7fca44f6c0
commit 95b514b336
11 changed files with 142 additions and 174 deletions

View File

@@ -1,98 +0,0 @@
import "./BeatLikeLoopSettings.css";
import BeatLike from "@/BeatLike";
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
import ISubscriber from "@/Subscriber";
import UINode, {UINodeOptions} from "@/ui/UINode";
import {BeatEvents} from "@/Beat";
import {IPublisher} from "@/Publisher";
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
export type BeatLikeLoopSettingsViewUINodeOptions = UINodeOptions & {
beatLike: BeatLike,
title?: string,
};
export default class BeatLikeLoopSettingsView extends UINode implements ISubscriber {
private beatLike: BeatLike;
private loopLengthInput!: NumberInputView;
private loopCheckbox!: BoolBoxView;
private loopLengthSection!: HTMLDivElement;
private title: string;
constructor(options: BeatLikeLoopSettingsViewUINodeOptions) {
super(options);
this.beatLike = options.beatLike;
this.title = options.title ?? "Looping settings:";
this.setupBindings();
}
private setupBindings() {
this.beatLike.addSubscriber(this, [
BeatEvents.LoopLengthChanged,
BeatEvents.DisplayTypeChanged
]);
}
notify<T extends string | number>(publisher: IPublisher<T> | null, event: "all" | T[] | T): void {
if (event === BeatEvents.LoopLengthChanged) {
this.loopLengthInput.setValue(this.beatLike.getLoopLength());
} else if (event === BeatEvents.DisplayTypeChanged) {
this.loopCheckbox.setValue(this.beatLike.isLooping());
if (this.beatLike.isLooping()) {
this.loopLengthSection.classList.remove("hide");
} else {
this.loopLengthSection.classList.add("hide");
}
}
}
setBeatLike(beatLike: BeatLike): void {
this.beatLike = beatLike;
this.notify(null, BeatEvents.LoopLengthChanged);
this.notify(null, BeatEvents.DisplayTypeChanged);
}
build(): HTMLElement {
this.loopLengthInput = new NumberInputView({
initialValue: this.beatLike.getLoopLength(),
label: "Length:",
onDecrement: () => this.beatLike.setLoopLength(this.beatLike.getLoopLength() - 1),
onIncrement: () => this.beatLike.setLoopLength(this.beatLike.getLoopLength() + 1),
onNewInput: (input: number) => this.beatLike.setLoopLength(input),
});
this.loopCheckbox = new BoolBoxView({
label: "On:",
value: this.beatLike.isLooping(),
onInput: (isChecked: boolean) => this.beatLike.setLooping(isChecked),
});
this.loopLengthSection = UINode.make("div", {
classes: ["loop-settings-option"],
subs: [
this.loopLengthInput.render(),
],
});
if (this.beatLike.isLooping()) {
this.loopLengthSection.classList.remove("hide");
} else {
this.loopLengthSection.classList.add("hide");
}
return UINode.make("div", {
classes: ["loop-settings"],
subs: [
UINode.make("p", {innerText: this.title}),
UINode.make("div", {
classes: ["loop-settings-option-group"],
subs: [
UINode.make("div", {
classes: ["loop-settings-option"],
subs: [
this.loopCheckbox.render(),
],
}),
this.loopLengthSection,
],
}),
]
});
}
}

View File

@@ -1,43 +1,40 @@
.beat-settings {
margin-bottom: 0.5em;
display: flex;
}
.beat-settings-title-input {
width: 100%;
height: 2em;
}
.beat-settings-title {
width: 100%;
font-weight: bold;
padding: 0.5em;
transition: background-color 200ms;
cursor: pointer;
}
.beat-settings-title:hover {
background-color: var(--color-ui-neutral-dark-hover);
}
.beat-settings-lower {
height: 3.5em;
display: flex;
text-align: center;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5em;
}
.beat-settings > * {
.beat-settings-lower > * {
margin-right: 0.2em;
}
.beat-settings:last-child {
.beat-settings-lower:last-child {
margin-right: 0;
}
.beat-settings-time-sig-up {
display: block;
}
.beat-settings-time-sig-down {
display: block;
}
.beat-settings-option {
display: inline-block;
}
.beat-settings-option-group {
align-self: stretch;
}
.beat-settings-time-sig input {
margin: auto;
}
.beat-settings-name-field {
width: 5em;
}
.beat-settings .loop-settings {
text-align: left;
flex: auto;

View File

@@ -13,15 +13,18 @@ export type BeatSettingsViewUINodeOptions = UINodeOptions & {
export default class BeatSettingsView extends UINode implements ISubscriber {
private beat: Beat;
private nameInput!: HTMLInputElement;
private deleteButton!: ActionButtonView;
private loopLengthInput!: NumberInputView;
private bakeButton!: ActionButtonView;
private loopCheckbox!: BoolBoxView;
private loopLengthSection!: HTMLDivElement;
private sub!: ISubscription;
private titleInput!: HTMLInputElement;
private titleDisplay!: HTMLSpanElement;
private editingTitle: boolean;
constructor(options: BeatSettingsViewUINodeOptions) {
super(options);
this.editingTitle = false;
this.beat = options.beat;
this.setupBindings();
}
@@ -41,11 +44,13 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
notify<T extends string | number>(publisher: IPublisher<T> | null, event: "all" | T[] | T): void {
if (event === BeatEvents.NewName) {
this.nameInput.value = this.beat.getName();
this.titleInput.value = this.beat.getName();
this.titleDisplay.innerText = this.beat.getName();
} else if (event === BeatEvents.LoopLengthChanged) {
this.loopLengthInput.setValue(this.beat.getLoopLength());
} else if (event === BeatEvents.DisplayTypeChanged) {
this.loopCheckbox.setValue(this.beat.isLooping());
this.bakeButton.setDisabled(!this.beat.isLooping());
if (this.beat.isLooping()) {
this.loopLengthSection.classList.remove("hide");
} else {
@@ -55,16 +60,34 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
}
build(): HTMLElement {
this.nameInput = UINode.make("input", {
this.titleInput = UINode.make("input", {
value: this.beat.getName(),
classes: ["beat-settings-name-field"],
classes: ["beat-settings-title-input"],
type: "text",
oninput: (event: Event) => this.beat.setName((event.target as HTMLInputElement).value),
oninput: (event: Event) => {
this.beat.setName((event.target as HTMLInputElement).value);
},
onblur: () => this.titleInput.replaceWith(this.titleDisplay),
onkeyup: (event: KeyboardEvent) => {
if (event.key === "Enter") {
(event.target as HTMLInputElement).blur();
}
}
});
this.deleteButton = new ActionButtonView({
icon: "trash",
this.titleDisplay = UINode.make("div", {
innerText: this.beat.getName(),
classes: ["beat-settings-title"],
onclick: () => {
this.titleDisplay.replaceWith(this.titleInput);
this.titleInput.focus();
}
});
this.bakeButton = new ActionButtonView({
icon: "snowflake",
type: "secondary",
onClick: () => this.beat.delete(),
alt: "Bake Loops",
disabled: !this.beat.isLooping(),
onClick: () => this.beat.bakeLoops(),
});
this.loopLengthInput = new NumberInputView({
initialValue: this.beat.getLoopLength(),
@@ -91,15 +114,26 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
return UINode.make("div", {
classes: ["beat-settings"],
subs: [
this.nameInput,
this.titleDisplay,
UINode.make("div", {
classes: ["loop-settings"],
classes: ["beat-settings-lower"],
subs: [
this.loopCheckbox.render(),
this.bakeButton.render(),
new ActionButtonView({
icon: "trash",
type: "secondary",
alt: "Delete Track",
onClick: () => this.beat.delete(),
}).render(),
UINode.make("div", {
classes: ["loop-settings"],
subs: [
this.loopCheckbox.render(),
]
}),
this.loopLengthSection,
]
}),
this.loopLengthSection,
this.deleteButton.render(),
],
});
}

View File

@@ -47,12 +47,12 @@ export default class RootView extends UINode {
subs: [
UINode.make("div", {
classes: ["root-hamburger"],
subs: [new IconView({iconName: "list"}).render()],
subs: [new IconView({iconName: "list", color: "var(--color-ui-neutral-dark)"}).render()],
onclick: () => this.toggleSidebar(),
}),
UINode.make("div", {
classes: ["root-switch-mode"],
subs: [new IconView({iconName: "arrowClockwise"}).render()],
subs: [new IconView({iconName: "arrowClockwise", color: "var(--color-ui-neutral-dark)"}).render()],
onclick: () => this.toggleOrientation(),
})
]

View File

@@ -5,17 +5,20 @@
border: none;
}
.action-button.disabled {
cursor: default;
opacity: 50%;
}
.action-button.action-button-primary {
background-color: var(--color-ui-accent);
color: var(--color-p-light);
}
.action-button.action-button-primary:hover {
.action-button.action-button-primary:hover:not.disabled {
background-color: var(--color-ui-accent-hover);
color: var(--color-p-light-hover);
}
.action-button.action-button-primary:active {
.action-button.action-button-primary:active:not.disabled {
background-color: var(--color-ui-accent-active);
color: var(--color-p-light-active);
}
@@ -24,13 +27,11 @@
background-color: var(--color-ui-neutral-dark);
color: var(--color-p-light);
}
.action-button.action-button-secondary:hover {
.action-button.action-button-secondary:hover:not.disabled {
background-color: var(--color-ui-neutral-dark-hover);
color: var(--color-p-light-hover);
}
.action-button.action-button-secondary:active {
.action-button.action-button-secondary:active:not.disabled {
background-color: var(--color-ui-neutral-dark-active);
color: var(--color-p-light-active);
}

View File

@@ -4,7 +4,9 @@ import IconView, {IconName} from "@/ui/Widgets/Icon/IconView";
export type ActionButtonUINodeOptions = UINodeOptions & {
type?: "primary" | "secondary",
onClick?: (isChecked: boolean) => void,
onClick?: (event: MouseEvent) => void,
alt?: string,
disabled?: boolean,
} & ({
icon: IconName,
label?: never,
@@ -17,8 +19,10 @@ export default class ActionButtonView extends UINode {
private label: string | null = null;
private icon: IconName | null = null;
private buttonElement!: HTMLButtonElement;
private onClick: (isChecked: boolean) => void;
private onClick: (event: MouseEvent) => void;
private type: "primary" | "secondary";
private alt: string | null;
private disabled: boolean;
constructor(options: ActionButtonUINodeOptions) {
super(options);
@@ -27,22 +31,41 @@ export default class ActionButtonView extends UINode {
} else if (typeof options.label !== "undefined") {
this.label = options.label;
}
this.disabled = options.disabled ?? false;
this.alt = options.alt ?? null;
this.type = options.type ?? "primary";
this.onClick = options.onClick ?? (() => { /* dummy */ });
}
setDisabled(isDisabled: boolean): void {
this.disabled = isDisabled;
this.buttonElement.disabled = this.disabled;
if (isDisabled) {
this.buttonElement.classList.add("disabled");
} else {
this.buttonElement.classList.remove("disabled");
}
}
protected build(): HTMLButtonElement {
this.buttonElement = UINode.make("button", {
classes: ["action-button", `action-button-${this.type}`],
onclick: this.onClick,
onclick: (event: MouseEvent) => this.disabled || this.onClick(event),
subs: [
this.icon !== null ? new IconView({
iconName: this.icon
iconName: this.icon,
color: "var(--color-p-light)",
}).render() : UINode.make("span", {
innerText: this.label ?? ""
}),
],
});
if (this.alt) {
this.buttonElement.title = this.alt;
}
if (this.disabled) {
this.buttonElement.classList.add("disabled");
}
return this.buttonElement;
}
}

View File

@@ -1,24 +1,26 @@
.bool-box {
height: 1.1em;
height: 1.5em;
position: relative;
white-space: nowrap;
margin: 0.5em;
line-height: 1em;
line-height: 1.5em;
cursor: pointer;
}
.bool-box-label {
position: relative;
display: inline-block;
margin-right: 0.5em;
margin-left: 0.5em;
top: -0.33em;
cursor: pointer;
}
input.bool-box-checkbox[type="checkbox"] {
position: relative;
display: inline-block;
width: 2em;
height: 1em;
width: 3em;
height: 1.5em;
padding: 0;
top: 0.1em;
margin: 0;
-webkit-appearance: none;
-moz-appearance: none;
@@ -27,9 +29,10 @@ input.bool-box-checkbox[type="checkbox"] {
}
input.bool-box-checkbox[type="checkbox"]::before {
width: 2em;
height: 1em;
margin: 0.1em;
top: 0.3em;
left: 0.3em;
width: 2.3em;
height: 0.9em;
border-radius: 1em;
background-color: var(--color-ui-accent-active);
display: inline-block;
@@ -46,21 +49,19 @@ input.bool-box-checkbox[type="checkbox"]:checked::before {
input.bool-box-checkbox[type="checkbox"]::after {
box-sizing: border-box;
position: absolute;
width: 1.2em;
height: 1.2em;
border-radius: 1em;
border-color: var(--color-ui-neutral-dark);
border-width: 0.075em;
border-style: solid;
width: 1.35em;
height: 1.35em;
border-radius: 100%;
background-color: var(--color-ui-neutral-dark);
display: block;
content: "";
left: -0.05em;
top: 0.075em;
left: 0.025em;
z-index: 1;
transition: left 200ms, background-color 200ms;
}
input.bool-box-checkbox[type="checkbox"]:checked::after {
left: 1.1em;
left: 1.575em;
background-color: var(--color-ui-neutral-light);
}

View File

@@ -1,8 +1,9 @@
.icon-view {
--icon-bg: black;
width: 2em;
height: 2em;
-webkit-mask-size: 2em;
mask-size: 2em;
display: inline-block;
background-color: black;
background-color: var(--icon-bg);
}

View File

@@ -3,24 +3,29 @@ import "./Icon.css";
import List from "./svgs/list.svg";
import ArrowClockwise from "./svgs/arrow-clockwise.svg";
import Trash from "./svgs/trash.svg";
import Snowflake from "./svgs/snowflake.svg";
const IconUrlMap = {
arrowClockwise: ArrowClockwise,
list: List,
trash: Trash,
snowflake: Snowflake,
} as const;
export type IconName = keyof typeof IconUrlMap;
export type IconViewOptions = UINodeOptions & {
iconName: IconName,
color?: string,
};
export default class IconView extends UINode {
private iconUrl: string;
private color: string | null;
constructor(options: IconViewOptions) {
super(options);
this.color = options.color ?? null;
this.iconUrl = IconUrlMap[options.iconName];
}
@@ -28,7 +33,8 @@ export default class IconView extends UINode {
const icon = UINode.make("div", {
classes: ["icon-view"],
});
icon.style.cssText = `-webkit-mask-image: url(${this.iconUrl}); mask-image: url(${this.iconUrl});`;
const colorString = this.color ? `--icon-bg:${this.color}` : "";
icon.style.cssText = `-webkit-mask-image: url(${this.iconUrl}); mask-image: url(${this.iconUrl});${colorString}`;
return icon;
}
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-snow2" viewBox="0 0 16 16">
<path d="M8 16a.5.5 0 0 1-.5-.5v-1.293l-.646.647a.5.5 0 0 1-.707-.708L7.5 12.793v-1.086l-.646.647a.5.5 0 0 1-.707-.708L7.5 10.293V8.866l-1.236.713-.495 1.85a.5.5 0 1 1-.966-.26l.237-.882-.94.542-.496 1.85a.5.5 0 1 1-.966-.26l.237-.882-1.12.646a.5.5 0 0 1-.5-.866l1.12-.646-.884-.237a.5.5 0 1 1 .26-.966l1.848.495.94-.542-.882-.237a.5.5 0 1 1 .258-.966l1.85.495L7 8l-1.236-.713-1.849.495a.5.5 0 1 1-.258-.966l.883-.237-.94-.542-1.85.495a.5.5 0 0 1-.258-.966l.883-.237-1.12-.646a.5.5 0 1 1 .5-.866l1.12.646-.237-.883a.5.5 0 0 1 .966-.258l.495 1.849.94.542-.236-.883a.5.5 0 0 1 .966-.258l.495 1.849 1.236.713V5.707L6.147 4.354a.5.5 0 1 1 .707-.708l.646.647V3.207L6.147 1.854a.5.5 0 1 1 .707-.708l.646.647V.5a.5.5 0 0 1 1 0v1.293l.647-.647a.5.5 0 1 1 .707.708L8.5 3.207v1.086l.647-.647a.5.5 0 1 1 .707.708L8.5 5.707v1.427l1.236-.713.495-1.85a.5.5 0 1 1 .966.26l-.236.882.94-.542.495-1.85a.5.5 0 1 1 .966.26l-.236.882 1.12-.646a.5.5 0 0 1 .5.866l-1.12.646.883.237a.5.5 0 1 1-.26.966l-1.848-.495-.94.542.883.237a.5.5 0 1 1-.26.966l-1.848-.495L9 8l1.236.713 1.849-.495a.5.5 0 0 1 .259.966l-.883.237.94.542 1.849-.495a.5.5 0 0 1 .259.966l-.883.237 1.12.646a.5.5 0 0 1-.5.866l-1.12-.646.236.883a.5.5 0 1 1-.966.258l-.495-1.849-.94-.542.236.883a.5.5 0 0 1-.966.258L9.736 9.58 8.5 8.866v1.427l1.354 1.353a.5.5 0 0 1-.707.708l-.647-.647v1.086l1.354 1.353a.5.5 0 0 1-.707.708l-.647-.647V15.5a.5.5 0 0 1-.5.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB