diff --git a/src/ui/BeatLikeLoopSettings/BeatLikeLoopSettings.css b/src/ui/BeatLikeLoopSettings/BeatLikeLoopSettings.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/ui/BeatLikeLoopSettings/BeatLikeLoopSettingsView.ts b/src/ui/BeatLikeLoopSettings/BeatLikeLoopSettingsView.ts deleted file mode 100644 index f44467b..0000000 --- a/src/ui/BeatLikeLoopSettings/BeatLikeLoopSettingsView.ts +++ /dev/null @@ -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(publisher: IPublisher | 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, - ], - }), - ] - }); - } -} diff --git a/src/ui/BeatSettings/BeatSettings.css b/src/ui/BeatSettings/BeatSettings.css index ed6fd8a..f3124a0 100644 --- a/src/ui/BeatSettings/BeatSettings.css +++ b/src/ui/BeatSettings/BeatSettings.css @@ -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; diff --git a/src/ui/BeatSettings/BeatSettingsView.ts b/src/ui/BeatSettings/BeatSettingsView.ts index 429233a..b3076ae 100644 --- a/src/ui/BeatSettings/BeatSettingsView.ts +++ b/src/ui/BeatSettings/BeatSettingsView.ts @@ -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(publisher: IPublisher | 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(), ], }); } diff --git a/src/ui/Root/RootView.ts b/src/ui/Root/RootView.ts index 54385c9..2fdc87b 100644 --- a/src/ui/Root/RootView.ts +++ b/src/ui/Root/RootView.ts @@ -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(), }) ] diff --git a/src/ui/Widgets/ActionButton/ActionButton.css b/src/ui/Widgets/ActionButton/ActionButton.css index bf7190c..9238689 100644 --- a/src/ui/Widgets/ActionButton/ActionButton.css +++ b/src/ui/Widgets/ActionButton/ActionButton.css @@ -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); } diff --git a/src/ui/Widgets/ActionButton/ActionButtonView.ts b/src/ui/Widgets/ActionButton/ActionButtonView.ts index 2c46678..f1281f0 100644 --- a/src/ui/Widgets/ActionButton/ActionButtonView.ts +++ b/src/ui/Widgets/ActionButton/ActionButtonView.ts @@ -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; } } diff --git a/src/ui/Widgets/BoolBox/BoolBox.css b/src/ui/Widgets/BoolBox/BoolBox.css index c86ee3b..47ea3ed 100644 --- a/src/ui/Widgets/BoolBox/BoolBox.css +++ b/src/ui/Widgets/BoolBox/BoolBox.css @@ -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); } \ No newline at end of file diff --git a/src/ui/Widgets/Icon/Icon.css b/src/ui/Widgets/Icon/Icon.css index 20f410b..823570b 100644 --- a/src/ui/Widgets/Icon/Icon.css +++ b/src/ui/Widgets/Icon/Icon.css @@ -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); } \ No newline at end of file diff --git a/src/ui/Widgets/Icon/IconView.ts b/src/ui/Widgets/Icon/IconView.ts index 14c3c8f..f4fd6af 100644 --- a/src/ui/Widgets/Icon/IconView.ts +++ b/src/ui/Widgets/Icon/IconView.ts @@ -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; } } \ No newline at end of file diff --git a/src/ui/Widgets/Icon/svgs/snowflake.svg b/src/ui/Widgets/Icon/svgs/snowflake.svg new file mode 100644 index 0000000..cede335 --- /dev/null +++ b/src/ui/Widgets/Icon/svgs/snowflake.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file