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:
@@ -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,
|
||||
],
|
||||
}),
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,7 +114,17 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
|
||||
return UINode.make("div", {
|
||||
classes: ["beat-settings"],
|
||||
subs: [
|
||||
this.nameInput,
|
||||
this.titleDisplay,
|
||||
UINode.make("div", {
|
||||
classes: ["beat-settings-lower"],
|
||||
subs: [
|
||||
this.bakeButton.render(),
|
||||
new ActionButtonView({
|
||||
icon: "trash",
|
||||
type: "secondary",
|
||||
alt: "Delete Track",
|
||||
onClick: () => this.beat.delete(),
|
||||
}).render(),
|
||||
UINode.make("div", {
|
||||
classes: ["loop-settings"],
|
||||
subs: [
|
||||
@@ -99,7 +132,8 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
|
||||
]
|
||||
}),
|
||||
this.loopLengthSection,
|
||||
this.deleteButton.render(),
|
||||
]
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
3
src/ui/Widgets/Icon/svgs/snowflake.svg
Normal file
3
src/ui/Widgets/Icon/svgs/snowflake.svg
Normal 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 |
Reference in New Issue
Block a user