Major frontend code cleanup (#452)

Many large changes, including:
- TypeScript enums are now string unions throughout
- Strong type-checking throughout the TS and Vue codebase
- Vue component props now all specify `as PropType<...>`
- Usage of annotated return types on all functions
- Sorting of JS import statements
- Explicit usage of Vue bind attribute function call arguments (`@click="foo"` is now `@click=(e) => foo(e)`)
- Much improved code quality related to the color picker
- Consistent camelCase Vue bind and v-model attributes
- Consistent Vue HTML attribute strings with single quotes
- Bug fix and clarity improvement with incorrect hint class parameters
- Empty Vue component objects like `props: {}` and `components: {}` removed
This commit is contained in:
Keavon Chambers
2022-01-02 06:00:02 -08:00
parent 1954aceb0e
commit 7e0cbb60b4
53 changed files with 842 additions and 946 deletions
@@ -1,5 +1,5 @@
<template>
<button class="icon-button" :class="`size-${String(size)}`" @click="(e) => action(e)">
<button class="icon-button" :class="`size-${size}`" @click="(e) => action(e)">
<IconLabel :icon="icon" />
</button>
</template>
@@ -57,16 +57,16 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import IconLabel, { IconName, IconSize } from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
props: {
action: { type: Function, required: true },
icon: { type: String, required: true },
size: { type: Number, required: true },
gapAfter: { type: Boolean, default: false },
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
icon: { type: String as PropType<IconName>, required: true },
size: { type: Number as PropType<IconSize>, required: true },
gapAfter: { type: Boolean as PropType<boolean>, default: false },
},
components: { IconLabel },
});
@@ -1,7 +1,7 @@
<template>
<div class="popover-button">
<IconButton :action="handleClick" :icon="icon" :size="16" data-hover-menu-spawner />
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Bottom" ref="floatingMenu">
<FloatingMenu :type="'Popover'" :direction="'Bottom'" ref="floatingMenu">
<slot></slot>
</FloatingMenu>
</div>
@@ -47,15 +47,12 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
export enum PopoverButtonIcon {
"DropdownArrow" = "DropdownArrow",
"VerticalEllipsis" = "VerticalEllipsis",
}
export type PopoverButtonIcon = "DropdownArrow" | "VerticalEllipsis";
export default defineComponent({
components: {
@@ -63,8 +60,8 @@ export default defineComponent({
IconButton,
},
props: {
action: { type: Function, required: false },
icon: { type: String, default: PopoverButtonIcon.DropdownArrow },
action: { type: Function as PropType<() => void>, required: false },
icon: { type: String as PropType<PopoverButtonIcon>, default: "DropdownArrow" },
},
methods: {
handleClick() {
@@ -73,11 +70,5 @@ export default defineComponent({
if (this.action) this.action();
},
},
data() {
return {
MenuDirection,
MenuType,
};
},
});
</script>
@@ -1,5 +1,5 @@
<template>
<button class="text-button" :class="{ emphasized, disabled }" :style="minWidth > 0 ? `min-width: ${minWidth}px` : ''" @click="action">
<button class="text-button" :class="{ emphasized, disabled }" :style="minWidth > 0 ? `min-width: ${minWidth}px` : ''" @click="(e) => action(e)">
<TextLabel>{{ label }}</TextLabel>
</button>
</template>
@@ -49,18 +49,18 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
export default defineComponent({
props: {
action: { type: Function, required: true },
label: { type: String, required: true },
emphasized: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
minWidth: { type: Number, default: 0 },
gapAfter: { type: Boolean, default: false },
action: { type: Function as PropType<(e: MouseEvent) => void>, required: true },
label: { type: String as PropType<string>, required: true },
emphasized: { type: Boolean as PropType<boolean>, default: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
minWidth: { type: Number as PropType<number>, default: 0 },
gapAfter: { type: Boolean as PropType<boolean>, default: false },
},
components: { TextLabel },
});
@@ -1,12 +1,12 @@
<template>
<div class="color-picker">
<div class="saturation-picker" ref="saturationPicker" data-picker-action="MoveSaturation" @pointerdown="onPointerDown">
<div class="saturation-picker" ref="saturationPicker" @pointerdown="(e) => onPointerDown(e)">
<div ref="saturationCursor" class="selection-circle"></div>
</div>
<div class="hue-picker" ref="huePicker" data-picker-action="MoveHue" @pointerdown="onPointerDown">
<div class="hue-picker" ref="huePicker" @pointerdown="(e) => onPointerDown(e)">
<div ref="hueCursor" class="selection-pincers"></div>
</div>
<div class="opacity-picker" ref="opacityPicker" data-picker-action="MoveOpacity" @pointerdown="onPointerDown">
<div class="opacity-picker" ref="opacityPicker" @pointerdown="(e) => onPointerDown(e)">
<div ref="opacityCursor" class="selection-pincers"></div>
</div>
</div>
@@ -117,43 +117,28 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import { hsvToRgb, rgbToHsv, isRGB } from "@/utilities/color";
import { RGBA } from "@/dispatcher/js-messages";
import { hsvaToRgba, rgbaToHsva } from "@/utilities/color";
import { clamp } from "@/utilities/math";
const enum ColorPickerState {
Idle = "Idle",
MoveHue = "MoveHue",
MoveOpacity = "MoveOpacity",
MoveSaturation = "MoveSaturation",
}
type ColorPickerState = "Idle" | "MoveHue" | "MoveOpacity" | "MoveSaturation";
// TODO: Clean up the fundamental code design in this file to simplify it and use better practices.
// TODO: Such as removing the `picker*` data variables and reducing the number of functions which call each other in weird, non-obvious ways.
export default defineComponent({
components: {},
props: {
color: { type: Object, required: true },
color: { type: Object as PropType<RGBA>, required: true },
},
data() {
return {
state: ColorPickerState.Idle,
// Disable proxy on this object
// https://v3.vuejs.org/api/options-data.html#data-2
// eslint-disable-next-line vue/no-reserved-keys
_: {
colorPicker: {
color: { h: 0, s: 0, v: 0, a: 1 },
hue: {
rect: { width: 0, height: 0, top: 0, left: 0 },
},
opacity: {
rect: { width: 0, height: 0, top: 0, left: 0 },
},
saturation: {
rect: { width: 0, height: 0, top: 0, left: 0 },
},
},
},
state: "Idle" as ColorPickerState,
pickerHSVA: { h: 0, s: 0, v: 0, a: 1 },
pickerHueRect: { width: 0, height: 0, top: 0, left: 0 },
pickerOpacityRect: { width: 0, height: 0, top: 0, left: 0 },
pickerSaturationRect: { width: 0, height: 0, top: 0, left: 0 },
};
},
mounted() {
@@ -171,116 +156,122 @@ export default defineComponent({
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
},
getRef<T>(name: string) {
return this.$refs[name] as T;
},
onPointerDown(e: PointerEvent) {
if (!(e.currentTarget instanceof Element)) return;
const picker = e.currentTarget.getAttribute("data-picker-action");
this.state = (() => {
switch (picker) {
case "MoveHue":
return ColorPickerState.MoveHue;
case "MoveOpacity":
return ColorPickerState.MoveOpacity;
case "MoveSaturation":
return ColorPickerState.MoveSaturation;
default:
return ColorPickerState.Idle;
}
})();
if (!(e.currentTarget instanceof HTMLElement)) return;
if (this.state !== ColorPickerState.Idle) {
this.addEvents();
this.updateRects();
this.onPointerMove(e);
if ((this.$refs.saturationPicker as HTMLElement).contains(e.currentTarget)) {
this.state = "MoveSaturation";
} else if ((this.$refs.huePicker as HTMLElement).contains(e.currentTarget)) {
this.state = "MoveHue";
} else if ((this.$refs.opacityPicker as HTMLElement).contains(e.currentTarget)) {
this.state = "MoveOpacity";
} else {
this.state = "Idle";
}
if (this.state === "Idle") return;
this.addEvents();
this.updateRects();
this.onPointerMove(e);
},
onPointerMove(e: PointerEvent) {
const { colorPicker } = this.$data._;
if (this.state === ColorPickerState.MoveHue) {
this.setHuePosition(e.clientY - colorPicker.hue.rect.top);
} else if (this.state === ColorPickerState.MoveOpacity) {
this.setOpacityPosition(e.clientY - colorPicker.opacity.rect.top);
} else if (this.state === ColorPickerState.MoveSaturation) {
this.setSaturationPosition(e.clientX - colorPicker.saturation.rect.left, e.clientY - colorPicker.saturation.rect.top);
switch (this.state) {
case "MoveHue":
this.setHueCursorPosition(e.clientY - this.pickerHueRect.top);
break;
case "MoveOpacity":
this.setOpacityCursorPosition(e.clientY - this.pickerOpacityRect.top);
break;
case "MoveSaturation":
this.setSaturationCursorPosition(e.clientX - this.pickerSaturationRect.left, e.clientY - this.pickerSaturationRect.top);
break;
default:
return;
}
if (this.state !== ColorPickerState.Idle) {
this.updateHue();
this.$emit("update:color", hsvToRgb(colorPicker.color));
}
this.updateHue();
// The `color` prop's watcher calls `this.updateColor()`
this.$emit("update:color", hsvaToRgba(this.pickerHSVA));
},
onPointerUp() {
if (this.state !== ColorPickerState.Idle) {
this.state = ColorPickerState.Idle;
this.removeEvents();
}
if (this.state === "Idle") return;
this.state = "Idle";
this.removeEvents();
},
updateRects() {
const { colorPicker } = this.$data._;
const saturationPicker = this.getRef<HTMLElement>("saturationPicker");
// Saturation
const saturationPicker = this.$refs.saturationPicker as HTMLElement;
const saturation = saturationPicker.getBoundingClientRect();
colorPicker.saturation.rect.width = saturation.width;
colorPicker.saturation.rect.height = saturation.height;
colorPicker.saturation.rect.left = saturation.left;
colorPicker.saturation.rect.top = saturation.top;
const huePicker = this.getRef<HTMLElement>("huePicker");
this.pickerSaturationRect.width = saturation.width;
this.pickerSaturationRect.height = saturation.height;
this.pickerSaturationRect.left = saturation.left;
this.pickerSaturationRect.top = saturation.top;
// Hue
const huePicker = this.$refs.huePicker as HTMLElement;
const hue = huePicker.getBoundingClientRect();
colorPicker.hue.rect.width = hue.width;
colorPicker.hue.rect.height = hue.height;
colorPicker.hue.rect.left = hue.left;
colorPicker.hue.rect.top = hue.top;
const opacityPicker = this.getRef<HTMLElement>("opacityPicker");
this.pickerHueRect.width = hue.width;
this.pickerHueRect.height = hue.height;
this.pickerHueRect.left = hue.left;
this.pickerHueRect.top = hue.top;
// Opacity
const opacityPicker = this.$refs.opacityPicker as HTMLElement;
const opacity = opacityPicker.getBoundingClientRect();
colorPicker.opacity.rect.width = opacity.width;
colorPicker.opacity.rect.height = opacity.height;
colorPicker.opacity.rect.left = opacity.left;
colorPicker.opacity.rect.top = opacity.top;
this.pickerOpacityRect.width = opacity.width;
this.pickerOpacityRect.height = opacity.height;
this.pickerOpacityRect.left = opacity.left;
this.pickerOpacityRect.top = opacity.top;
},
setSaturationPosition(x: number, y: number) {
const { colorPicker } = this.$data._;
const saturationCursor = this.getRef<HTMLElement>("saturationCursor");
const saturationPosition = [clamp(x, 0, colorPicker.saturation.rect.width), clamp(y, 0, colorPicker.saturation.rect.height)];
saturationCursor.style.transform = `translate(${saturationPosition[0]}px, ${saturationPosition[1]}px)`;
colorPicker.color.s = saturationPosition[0] / colorPicker.saturation.rect.width;
colorPicker.color.v = (1 - saturationPosition[1] / colorPicker.saturation.rect.height) * 255;
setSaturationCursorPosition(x: number, y: number) {
const saturationPositionX = clamp(x, 0, this.pickerSaturationRect.width);
const saturationPositionY = clamp(y, 0, this.pickerSaturationRect.height);
const saturationCursor = this.$refs.saturationCursor as HTMLElement;
saturationCursor.style.transform = `translate(${saturationPositionX}px, ${saturationPositionY}px)`;
this.pickerHSVA.s = saturationPositionX / this.pickerSaturationRect.width;
this.pickerHSVA.v = (1 - saturationPositionY / this.pickerSaturationRect.height) * 255;
},
setHuePosition(y: number) {
const { colorPicker } = this.$data._;
const hueCursor = this.getRef<HTMLElement>("hueCursor");
const huePosition = clamp(y, 0, colorPicker.hue.rect.height);
setHueCursorPosition(y: number) {
const huePosition = clamp(y, 0, this.pickerHueRect.height);
const hueCursor = this.$refs.hueCursor as HTMLElement;
hueCursor.style.transform = `translateY(${huePosition}px)`;
colorPicker.color.h = clamp(1 - huePosition / colorPicker.hue.rect.height);
this.pickerHSVA.h = clamp(1 - huePosition / this.pickerHueRect.height);
},
setOpacityPosition(y: number) {
const { colorPicker } = this.$data._;
const opacityCursor = this.getRef<HTMLElement>("opacityCursor");
const opacityPosition = clamp(y, 0, colorPicker.opacity.rect.height);
setOpacityCursorPosition(y: number) {
const opacityPosition = clamp(y, 0, this.pickerOpacityRect.height);
const opacityCursor = this.$refs.opacityCursor as HTMLElement;
opacityCursor.style.transform = `translateY(${opacityPosition}px)`;
colorPicker.color.a = clamp(1 - opacityPosition / colorPicker.opacity.rect.height);
this.pickerHSVA.a = clamp(1 - opacityPosition / this.pickerOpacityRect.height);
},
updateHue() {
const { colorPicker } = this.$data._;
let color = hsvToRgb({ h: colorPicker.color.h, s: 1, v: 255, a: 1 });
this.$el.style.setProperty("--saturation-picker-hue", `rgb(${color.r}, ${color.g}, ${color.b})`);
color = hsvToRgb(colorPicker.color);
this.$el.style.setProperty("--opacity-picker-color", `rgb(${color.r}, ${color.g}, ${color.b})`);
const hsva = hsvaToRgba({ h: this.pickerHSVA.h, s: 1, v: 255, a: 1 });
const rgba = hsvaToRgba(this.pickerHSVA);
this.$el.style.setProperty("--saturation-picker-hue", `rgb(${hsva.r}, ${hsva.g}, ${hsva.b})`);
this.$el.style.setProperty("--opacity-picker-color", `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})`);
},
updateColor() {
if (this.state !== ColorPickerState.Idle) return;
const { color } = this;
if (!isRGB(color)) return;
const { colorPicker } = this.$data._;
colorPicker.color = rgbToHsv(color);
if (this.state !== "Idle") return;
this.pickerHSVA = rgbaToHsva(this.color);
this.updateRects();
this.setSaturationPosition(colorPicker.color.s * colorPicker.saturation.rect.width, (1 - colorPicker.color.v / 255) * colorPicker.saturation.rect.height);
this.setOpacityPosition((1 - colorPicker.color.a) * colorPicker.opacity.rect.height);
this.setHuePosition((1 - colorPicker.color.h) * colorPicker.hue.rect.height);
this.setSaturationCursorPosition(this.pickerHSVA.s * this.pickerSaturationRect.width, (1 - this.pickerHSVA.v / 255) * this.pickerSaturationRect.height);
this.setOpacityCursorPosition((1 - this.pickerHSVA.a) * this.pickerOpacityRect.height);
this.setHueCursorPosition((1 - this.pickerHSVA.h) * this.pickerHueRect.height);
this.updateHue();
},
},
@@ -1,6 +1,6 @@
<template>
<div class="dialog-modal">
<FloatingMenu :type="MenuType.Dialog" :direction="MenuDirection.Center">
<FloatingMenu :type="'Dialog'" :direction="'Center'">
<LayoutRow>
<LayoutCol :class="'icon-column'">
<!-- `dialog.state.icon` class exists to provide special sizing in CSS to specific icons -->
@@ -79,12 +79,12 @@
<script lang="ts">
import { defineComponent } from "vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
export default defineComponent({
inject: ["dialog"],
@@ -101,11 +101,5 @@ export default defineComponent({
this.dialog.dismissDialog();
},
},
data() {
return {
MenuDirection,
MenuType,
};
},
});
</script>
@@ -1,6 +1,6 @@
<template>
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open || type === MenuType.Dialog" ref="floatingMenu">
<div class="tail" v-if="type === MenuType.Popover"></div>
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open || type === 'Dialog'" ref="floatingMenu">
<div class="tail" v-if="type === 'Popover'"></div>
<div class="floating-menu-container" ref="floatingMenuContainer">
<div class="floating-menu-content" :class="{ 'scrollable-y': scrollable }" ref="floatingMenuContent" :style="floatingMenuContentStyle">
<slot></slot>
@@ -177,36 +177,20 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
export enum MenuDirection {
Top = "Top",
Bottom = "Bottom",
Left = "Left",
Right = "Right",
TopLeft = "TopLeft",
TopRight = "TopRight",
BottomLeft = "BottomLeft",
BottomRight = "BottomRight",
Center = "Center",
}
export enum MenuType {
Popover = "Popover",
Dropdown = "Dropdown",
Dialog = "Dialog",
}
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center";
export type MenuType = "Popover" | "Dropdown" | "Dialog";
const POINTER_STRAY_DISTANCE = 100;
export default defineComponent({
components: {},
props: {
direction: { type: String, default: MenuDirection.Bottom },
type: { type: String, required: true },
windowEdgeMargin: { type: Number, default: 6 },
minWidth: { type: Number, default: 0 },
scrollable: { type: Boolean, default: false },
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
type: { type: String as PropType<MenuType>, required: true },
windowEdgeMargin: { type: Number as PropType<number>, default: 6 },
minWidth: { type: Number as PropType<number>, default: 0 },
scrollable: { type: Boolean as PropType<boolean>, default: false },
},
data() {
const containerResizeObserver = new ResizeObserver((entries) => {
@@ -218,8 +202,6 @@ export default defineComponent({
open: false,
pointerStillDown: false,
containerResizeObserver,
MenuDirection,
MenuType,
};
},
updated() {
@@ -235,8 +217,8 @@ export default defineComponent({
let zeroedBorderDirection1: Edge | undefined;
let zeroedBorderDirection2: Edge | undefined;
if (this.direction === MenuDirection.Top || this.direction === MenuDirection.Bottom) {
zeroedBorderDirection1 = this.direction === MenuDirection.Top ? "Bottom" : "Top";
if (this.direction === "Top" || this.direction === "Bottom") {
zeroedBorderDirection1 = this.direction === "Top" ? "Bottom" : "Top";
if (floatingMenuBounds.left - this.windowEdgeMargin <= workspaceBounds.left) {
floatingMenuContent.style.left = `${this.windowEdgeMargin}px`;
@@ -249,8 +231,8 @@ export default defineComponent({
}
}
if (this.direction === MenuDirection.Left || this.direction === MenuDirection.Right) {
zeroedBorderDirection2 = this.direction === MenuDirection.Left ? "Right" : "Left";
if (this.direction === "Left" || this.direction === "Right") {
zeroedBorderDirection2 = this.direction === "Left" ? "Right" : "Left";
if (floatingMenuBounds.top - this.windowEdgeMargin <= workspaceBounds.top) {
floatingMenuContent.style.top = `${this.windowEdgeMargin}px`;
@@ -264,7 +246,7 @@ export default defineComponent({
}
// Remove the rounded corner from where the tail perfectly meets the corner
if (this.type === MenuType.Popover && this.windowEdgeMargin === 6 && zeroedBorderDirection1 && zeroedBorderDirection2) {
if (this.type === "Popover" && this.windowEdgeMargin === 6 && zeroedBorderDirection1 && zeroedBorderDirection2) {
switch (`${zeroedBorderDirection1}${zeroedBorderDirection2}`) {
case "TopLeft":
floatingMenuContent.style.borderTopLeftRadius = "0";
@@ -1,7 +1,7 @@
<template>
<FloatingMenu :class="'menu-list'" :direction="direction" :type="MenuType.Dropdown" ref="floatingMenu" :windowEdgeMargin="0" :scrollable="scrollable" data-hover-menu-keep-open>
<FloatingMenu :class="'menu-list'" :direction="direction" :type="'Dropdown'" ref="floatingMenu" :windowEdgeMargin="0" :scrollable="scrollable" data-hover-menu-keep-open>
<template v-for="(section, sectionIndex) in menuEntries" :key="sectionIndex">
<Separator :type="SeparatorType.List" :direction="SeparatorDirection.Vertical" v-if="sectionIndex > 0" />
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
<div
v-for="(entry, entryIndex) in section"
:key="entryIndex"
@@ -26,7 +26,7 @@
<MenuList
v-if="entry.children"
:direction="MenuDirection.TopRight"
:direction="'TopRight'"
:menuEntries="entry.children"
v-bind="{ defaultAction, minWidth, drawIcon, scrollable }"
:ref="(ref) => setEntryRefs(entry, ref)"
@@ -132,13 +132,11 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import FloatingMenu, { MenuDirection } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
export type MenuListEntries<Value = string> = MenuListEntry<Value>[];
export type SectionsOfMenuListEntries<Value = string> = MenuListEntries<Value>[];
@@ -162,13 +160,13 @@ const KEYBOARD_LOCK_SWITCH_BROWSER = "This hotkey is reserved by the browser, bu
const MenuList = defineComponent({
inject: ["fullscreen"],
props: {
direction: { type: String as PropType<MenuDirection>, default: MenuDirection.Bottom },
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
activeEntry: { type: Object as PropType<MenuListEntry>, required: false },
defaultAction: { type: Function as PropType<() => void | undefined>, required: false },
minWidth: { type: Number, default: 0 },
drawIcon: { type: Boolean, default: false },
scrollable: { type: Boolean, default: false },
defaultAction: { type: Function as PropType<() => void>, required: false },
minWidth: { type: Number as PropType<number>, default: 0 },
drawIcon: { type: Boolean as PropType<boolean>, default: false },
scrollable: { type: Boolean as PropType<boolean>, default: false },
},
methods: {
setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu) {
@@ -231,7 +229,7 @@ const MenuList = defineComponent({
// Restore open/closed state if it was forced open for measurement
if (!initiallyOpen) floatingMenu.setClosed();
this.$emit("width-changed", width);
this.$emit("widthChanged", width);
});
});
},
@@ -265,10 +263,6 @@ const MenuList = defineComponent({
data() {
return {
keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
SeparatorDirection,
SeparatorType,
MenuDirection,
MenuType,
};
},
components: {
@@ -80,9 +80,9 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import IconLabel, { IconName } from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
data() {
@@ -96,9 +96,9 @@ export default defineComponent({
},
},
props: {
checked: { type: Boolean, required: true },
icon: { type: String, default: "Checkmark" },
outlineStyle: { type: Boolean, default: false },
checked: { type: Boolean as PropType<boolean>, required: true },
icon: { type: String as PropType<IconName>, default: "Checkmark" },
outlineStyle: { type: Boolean as PropType<boolean>, default: false },
},
components: { IconLabel },
});
@@ -1,16 +1,16 @@
<template>
<div class="dropdown-input">
<div class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px`, disabled: 'disabled' }" @click="clickDropdownBox" data-hover-menu-spawner>
<div class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px`, disabled: 'disabled' }" @click="() => clickDropdownBox()" data-hover-menu-spawner>
<IconLabel :class="'dropdown-icon'" :icon="activeEntry.icon" v-if="activeEntry.icon" />
<span>{{ activeEntry.label }}</span>
<IconLabel :class="'dropdown-arrow'" :icon="'DropdownArrow'" />
</div>
<MenuList
v-model:active-entry="activeEntry"
@update:activeEntry="activeEntryChanged"
@width-changed="onWidthChanged"
v-model:activeEntry="activeEntry"
@update:activeEntry="(newActiveEntry) => activeEntryChanged(newActiveEntry)"
@widthChanged="(newWidth) => onWidthChanged(newWidth)"
:menuEntries="menuEntries"
:direction="MenuDirection.Bottom"
:direction="'Bottom'"
:drawIcon="drawIcon"
:scrollable="true"
ref="menuList"
@@ -90,21 +90,19 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import MenuList, { MenuListEntry, SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
import { MenuDirection } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
props: {
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
selectedIndex: { type: Number, required: true },
drawIcon: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
selectedIndex: { type: Number as PropType<number>, required: true },
drawIcon: { type: Boolean as PropType<boolean>, default: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
},
data() {
return {
activeEntry: this.menuEntries.flat()[this.selectedIndex],
MenuDirection,
minWidth: 0,
};
},
@@ -10,7 +10,7 @@
<IconLabel :icon="entry.icon" v-if="entry.icon" />
<span v-if="entry.label">{{ entry.label }}</span>
</div>
<MenuList :menuEntries="entry.children" :direction="MenuDirection.Bottom" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref) => setEntryRefs(entry, ref)" />
<MenuList :menuEntries="entry.children" :direction="'Bottom'" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref) => setEntryRefs(entry, ref)" />
</div>
</div>
</template>
@@ -53,12 +53,11 @@
<script lang="ts">
import { defineComponent } from "vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import { ApplicationPlatform } from "@/components/window/MainWindow.vue";
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
import { MenuDirection } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import { EditorState } from "@/state/wasm-loader";
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
function makeMenuEntries(editor: EditorState): MenuListEntries {
return [
{
@@ -66,8 +65,8 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
ref: undefined,
children: [
[
{ label: "New", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: async () => editor.instance.new_document() },
{ label: "Open…", shortcut: ["KeyControl", "KeyO"], action: async () => editor.instance.open_document() },
{ label: "New", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: async (): Promise<void> => editor.instance.new_document() },
{ label: "Open…", shortcut: ["KeyControl", "KeyO"], action: async (): Promise<void> => editor.instance.open_document() },
{
label: "Open Recent",
shortcut: ["KeyControl", "KeyShift", "KeyO"],
@@ -84,18 +83,18 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
},
],
[
{ label: "Close", shortcut: ["KeyControl", "KeyW"], shortcutRequiresLock: true, action: async () => editor.instance.close_active_document_with_confirmation() },
{ label: "Close All", shortcut: ["KeyControl", "KeyAlt", "KeyW"], action: async () => editor.instance.close_all_documents_with_confirmation() },
{ label: "Close", shortcut: ["KeyControl", "KeyW"], shortcutRequiresLock: true, action: async (): Promise<void> => editor.instance.close_active_document_with_confirmation() },
{ label: "Close All", shortcut: ["KeyControl", "KeyAlt", "KeyW"], action: async (): Promise<void> => editor.instance.close_all_documents_with_confirmation() },
],
[
{ label: "Save", shortcut: ["KeyControl", "KeyS"], action: async () => editor.instance.save_document() },
{ label: "Save As…", shortcut: ["KeyControl", "KeyShift", "KeyS"], action: async () => editor.instance.save_document() },
{ label: "Save", shortcut: ["KeyControl", "KeyS"], action: async (): Promise<void> => editor.instance.save_document() },
{ label: "Save As…", shortcut: ["KeyControl", "KeyShift", "KeyS"], action: async (): Promise<void> => editor.instance.save_document() },
{ label: "Save All", shortcut: ["KeyControl", "KeyAlt", "KeyS"] },
{ label: "Auto-Save", checkbox: true, checked: true },
],
[
{ label: "Import…", shortcut: ["KeyControl", "KeyI"] },
{ label: "Export…", shortcut: ["KeyControl", "KeyE"], action: async () => editor.instance.export_document() },
{ label: "Export…", shortcut: ["KeyControl", "KeyE"], action: async (): Promise<void> => editor.instance.export_document() },
],
[{ label: "Quit", shortcut: ["KeyControl", "KeyQ"] }],
],
@@ -105,13 +104,13 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
ref: undefined,
children: [
[
{ label: "Undo", shortcut: ["KeyControl", "KeyZ"], action: async () => editor.instance.undo() },
{ label: "Redo", shortcut: ["KeyControl", "KeyShift", "KeyZ"], action: async () => editor.instance.redo() },
{ label: "Undo", shortcut: ["KeyControl", "KeyZ"], action: async (): Promise<void> => editor.instance.undo() },
{ label: "Redo", shortcut: ["KeyControl", "KeyShift", "KeyZ"], action: async (): Promise<void> => editor.instance.redo() },
],
[
{ label: "Cut", shortcut: ["KeyControl", "KeyX"], action: async () => editor.instance.cut() },
{ label: "Copy", icon: "Copy", shortcut: ["KeyControl", "KeyC"], action: async () => editor.instance.copy() },
{ label: "Paste", icon: "Paste", shortcut: ["KeyControl", "KeyV"], action: async () => editor.instance.paste() },
{ label: "Cut", shortcut: ["KeyControl", "KeyX"], action: async (): Promise<void> => editor.instance.cut() },
{ label: "Copy", icon: "Copy", shortcut: ["KeyControl", "KeyC"], action: async (): Promise<void> => editor.instance.copy() },
{ label: "Paste", icon: "Paste", shortcut: ["KeyControl", "KeyV"], action: async (): Promise<void> => editor.instance.paste() },
],
],
},
@@ -120,8 +119,8 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
ref: undefined,
children: [
[
{ label: "Select All", shortcut: ["KeyControl", "KeyA"], action: async () => editor.instance.select_all_layers() },
{ label: "Deselect All", shortcut: ["KeyControl", "KeyAlt", "KeyA"], action: async () => editor.instance.deselect_all_layers() },
{ label: "Select All", shortcut: ["KeyControl", "KeyA"], action: async (): Promise<void> => editor.instance.select_all_layers() },
{ label: "Deselect All", shortcut: ["KeyControl", "KeyAlt", "KeyA"], action: async (): Promise<void> => editor.instance.deselect_all_layers() },
{
label: "Order",
children: [
@@ -129,14 +128,14 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
{
label: "Raise To Front",
shortcut: ["KeyControl", "KeyShift", "KeyLeftBracket"],
action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_max()),
action: async (): Promise<void> => editor.instance.reorder_selected_layers(editor.rawWasm.i32_max()),
},
{ label: "Raise", shortcut: ["KeyControl", "KeyRightBracket"], action: async () => editor.instance.reorder_selected_layers(1) },
{ label: "Lower", shortcut: ["KeyControl", "KeyLeftBracket"], action: async () => editor.instance.reorder_selected_layers(-1) },
{ label: "Raise", shortcut: ["KeyControl", "KeyRightBracket"], action: async (): Promise<void> => editor.instance.reorder_selected_layers(1) },
{ label: "Lower", shortcut: ["KeyControl", "KeyLeftBracket"], action: async (): Promise<void> => editor.instance.reorder_selected_layers(-1) },
{
label: "Lower to Back",
shortcut: ["KeyControl", "KeyShift", "KeyRightBracket"],
action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_min()),
action: async (): Promise<void> => editor.instance.reorder_selected_layers(editor.rawWasm.i32_min()),
},
],
],
@@ -158,12 +157,12 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
label: "Help",
ref: undefined,
children: [
[{ label: "About Graphite", action: async () => editor.instance.request_about_graphite_dialog() }],
[{ label: "About Graphite", action: async (): Promise<void> => editor.instance.request_about_graphite_dialog() }],
[
{ label: "Report a Bug", action: () => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
{ label: "Visit on GitHub", action: () => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
{ label: "Report a Bug", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
{ label: "Visit on GitHub", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
],
[{ label: "Debug: Panic (DANGER)", action: async () => editor.rawWasm.intentional_panic() }],
[{ label: "Debug: Panic (DANGER)", action: async (): Promise<void> => editor.rawWasm.intentional_panic() }],
],
},
];
@@ -186,10 +185,8 @@ export default defineComponent({
},
data() {
return {
ApplicationPlatform,
menuEntries: makeMenuEntries(this.editor),
MenuDirection,
comingSoon: () => this.dialog.comingSoon(),
comingSoon: (): void => this.dialog.comingSoon(),
};
},
components: {
@@ -12,8 +12,8 @@
:disabled="disabled"
/>
<label v-if="label" :for="`number-input-${id}`">{{ label }}</label>
<button v-if="!Number.isNaN(value)" class="arrow left" @click="onIncrement(IncrementDirection.Decrease)"></button>
<button v-if="!Number.isNaN(value)" class="arrow right" @click="onIncrement(IncrementDirection.Increase)"></button>
<button v-if="!Number.isNaN(value)" class="arrow left" @click="onIncrement('Decrease')"></button>
<button v-if="!Number.isNaN(value)" class="arrow right" @click="onIncrement('Increase')"></button>
</div>
</template>
@@ -152,40 +152,29 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
export enum IncrementBehavior {
Add = "Add",
Multiply = "Multiply",
Callback = "Callback",
None = "None",
}
export enum IncrementDirection {
Decrease = "Decrease",
Increase = "Increase",
}
export type IncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
export type IncrementDirection = "Decrease" | "Increase";
export default defineComponent({
components: {},
props: {
value: { type: Number, required: true },
min: { type: Number, required: false },
max: { type: Number, required: false },
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: IncrementBehavior.Add },
incrementFactor: { type: Number, default: 1 },
incrementCallbackIncrease: { type: Function, required: false },
incrementCallbackDecrease: { type: Function, required: false },
isInteger: { type: Boolean, default: false },
unit: { type: String, default: "" },
unitIsHiddenWhenEditing: { type: Boolean, default: true },
displayDecimalPlaces: { type: Number, default: 3 },
label: { type: String, required: false },
disabled: { type: Boolean, default: false },
value: { type: Number as PropType<number>, required: true },
min: { type: Number as PropType<number>, required: false },
max: { type: Number as PropType<number>, required: false },
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
incrementFactor: { type: Number as PropType<number>, default: 1 },
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
incrementCallbackDecrease: { type: Function as PropType<() => void>, required: false },
isInteger: { type: Boolean as PropType<boolean>, default: false },
unit: { type: String as PropType<string>, default: "" },
unitIsHiddenWhenEditing: { type: Boolean as PropType<boolean>, default: true },
displayDecimalPlaces: { type: Number as PropType<number>, default: 3 },
label: { type: String as PropType<string>, required: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
},
data() {
return {
text: `${this.value}${this.unit}`,
editing: false,
IncrementDirection,
id: `${Math.random()}`.substring(2),
};
},
@@ -225,19 +214,19 @@ export default defineComponent({
if (Number.isNaN(this.value)) return;
switch (this.incrementBehavior) {
case IncrementBehavior.Add: {
const directionAddend = direction === IncrementDirection.Increase ? this.incrementFactor : -this.incrementFactor;
case "Add": {
const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor;
this.updateValue(this.value + directionAddend);
break;
}
case IncrementBehavior.Multiply: {
const directionMultiplier = direction === IncrementDirection.Increase ? this.incrementFactor : 1 / this.incrementFactor;
case "Multiply": {
const directionMultiplier = direction === "Increase" ? this.incrementFactor : 1 / this.incrementFactor;
this.updateValue(this.value * directionMultiplier);
break;
}
case IncrementBehavior.Callback: {
if (direction === IncrementDirection.Increase && this.incrementCallbackIncrease) this.incrementCallbackIncrease();
if (direction === IncrementDirection.Decrease && this.incrementCallbackDecrease) this.incrementCallbackDecrease();
case "Callback": {
if (direction === "Increase" && this.incrementCallbackIncrease) this.incrementCallbackIncrease();
if (direction === "Decrease" && this.incrementCallbackDecrease) this.incrementCallbackDecrease();
break;
}
default:
@@ -34,14 +34,15 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
import { IconName } from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
props: {
checked: { type: Boolean, required: true },
icon: { type: String, default: "Checkmark" },
checked: { type: Boolean as PropType<boolean>, required: true },
icon: { type: String as PropType<IconName>, default: "Checkmark" },
},
components: {
CheckboxInput,
@@ -85,7 +85,7 @@ export type RadioEntries = RadioEntryData[];
export default defineComponent({
props: {
entries: { type: Array as PropType<RadioEntries>, required: true },
selectedIndex: { type: Number, required: true },
selectedIndex: { type: Number as PropType<number>, required: true },
},
methods: {
handleEntryClick(menuEntry: RadioEntryData) {
@@ -29,16 +29,17 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import { IconName } from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
components: { IconButton },
props: {
icon: { type: String, required: true },
action: { type: Function, required: true },
active: { type: Boolean, default: false },
icon: { type: String as PropType<IconName>, required: true },
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
active: { type: Boolean as PropType<boolean>, default: false },
},
});
</script>
@@ -1,15 +1,15 @@
<template>
<div class="swatch-pair">
<div class="secondary swatch">
<button @click="clickSecondarySwatch" ref="secondaryButton" data-hover-menu-spawner></button>
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Right" horizontal ref="secondarySwatchFloatingMenu">
<ColorPicker @update:color="secondaryColorChanged" :color="secondaryColor" />
<button @click="() => clickSecondarySwatch()" ref="secondaryButton" data-hover-menu-spawner></button>
<FloatingMenu :type="'Popover'" :direction="'Right'" horizontal ref="secondarySwatchFloatingMenu">
<ColorPicker @update:color="(color) => secondaryColorChanged(color)" :color="secondaryColor" />
</FloatingMenu>
</div>
<div class="primary swatch">
<button @click="clickPrimarySwatch" ref="primaryButton" data-hover-menu-spawner></button>
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Right" horizontal ref="primarySwatchFloatingMenu">
<ColorPicker @update:color="primaryColorChanged" :color="primaryColor" />
<button @click="() => clickPrimarySwatch()" ref="primaryButton" data-hover-menu-spawner></button>
<FloatingMenu :type="'Popover'" :direction="'Right'" horizontal ref="primarySwatchFloatingMenu">
<ColorPicker @update:color="(color) => primaryColorChanged(color)" :color="primaryColor" />
</FloatingMenu>
</div>
</div>
@@ -68,11 +68,11 @@
<script lang="ts">
import { defineComponent } from "vue";
import { rgbToDecimalRgb, RGB } from "@/utilities/color";
import { RGBA, UpdateWorkingColors } from "@/dispatcher/js-messages";
import { rgbaToDecimalRgba } from "@/utilities/color";
import ColorPicker from "@/components/widgets/floating-menus/ColorPicker.vue";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import { UpdateWorkingColors } from "@/dispatcher/js-messages";
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
export default defineComponent({
inject: ["editor"],
@@ -80,70 +80,56 @@ export default defineComponent({
FloatingMenu,
ColorPicker,
},
props: {},
methods: {
clickPrimarySwatch() {
this.getRef<typeof FloatingMenu>("primarySwatchFloatingMenu").setOpen();
this.getRef<typeof FloatingMenu>("secondarySwatchFloatingMenu").setClosed();
(this.$refs.primarySwatchFloatingMenu as typeof FloatingMenu).setOpen();
(this.$refs.secondarySwatchFloatingMenu as typeof FloatingMenu).setClosed();
},
clickSecondarySwatch() {
this.getRef<typeof FloatingMenu>("secondarySwatchFloatingMenu").setOpen();
this.getRef<typeof FloatingMenu>("primarySwatchFloatingMenu").setClosed();
(this.$refs.secondarySwatchFloatingMenu as typeof FloatingMenu).setOpen();
(this.$refs.primarySwatchFloatingMenu as typeof FloatingMenu).setClosed();
},
getRef<T>(name: string) {
return this.$refs[name] as T;
},
primaryColorChanged(color: RGB) {
primaryColorChanged(color: RGBA) {
this.primaryColor = color;
this.updatePrimaryColor();
},
secondaryColorChanged(color: RGB) {
secondaryColorChanged(color: RGBA) {
this.secondaryColor = color;
this.updateSecondaryColor();
},
async updatePrimaryColor() {
let color = this.primaryColor;
const button = this.getRef<HTMLButtonElement>("primaryButton");
const button = this.$refs.primaryButton as HTMLButtonElement;
button.style.setProperty("--swatch-color", `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);
color = rgbToDecimalRgb(this.primaryColor);
color = rgbaToDecimalRgba(this.primaryColor);
this.editor.instance.update_primary_color(color.r, color.g, color.b, color.a);
},
async updateSecondaryColor() {
let color = this.secondaryColor;
const button = this.getRef<HTMLButtonElement>("secondaryButton");
const button = this.$refs.secondaryButton as HTMLButtonElement;
button.style.setProperty("--swatch-color", `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);
color = rgbToDecimalRgb(this.secondaryColor);
color = rgbaToDecimalRgba(this.secondaryColor);
this.editor.instance.update_secondary_color(color.r, color.g, color.b, color.a);
},
},
data() {
return {
MenuDirection,
MenuType,
primaryColor: { r: 0, g: 0, b: 0, a: 1 },
secondaryColor: { r: 255, g: 255, b: 255, a: 1 },
primaryColor: { r: 0, g: 0, b: 0, a: 1 } as RGBA,
secondaryColor: { r: 255, g: 255, b: 255, a: 1 } as RGBA,
};
},
mounted() {
this.editor.dispatcher.subscribeJsMessage(UpdateWorkingColors, (updateWorkingColors) => {
const { primary, secondary } = updateWorkingColors;
this.primaryColor = updateWorkingColors.primary.toRgba();
this.secondaryColor = updateWorkingColors.secondary.toRgba();
this.primaryColor = primary.toRgba();
this.secondaryColor = secondary.toRgba();
const primaryButton = this.$refs.primaryButton as HTMLButtonElement;
primaryButton.style.setProperty("--swatch-color", updateWorkingColors.primary.toRgbaCSS());
const primaryButton = this.getRef<HTMLButtonElement>("primaryButton");
primaryButton.style.setProperty("--swatch-color", primary.toRgbaCSS());
const secondaryButton = this.getRef<HTMLButtonElement>("secondaryButton");
secondaryButton.style.setProperty("--swatch-color", secondary.toRgbaCSS());
const secondaryButton = this.$refs.secondaryButton as HTMLButtonElement;
secondaryButton.style.setProperty("--swatch-color", updateWorkingColors.secondary.toRgbaCSS());
});
this.updatePrimaryColor();
@@ -1,5 +1,5 @@
<template>
<div class="icon-label" :class="`size-${String(icons[icon].size)}`">
<div class="icon-label" :class="`size-${icons[icon].size}`">
<component :is="icon" />
</div>
</template>
@@ -28,81 +28,19 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import LayoutSelectTool from "@/../assets/24px-two-tone/layout-select-tool.svg";
import LayoutCropTool from "@/../assets/24px-two-tone/layout-crop-tool.svg";
import LayoutNavigateTool from "@/../assets/24px-two-tone/layout-navigate-tool.svg";
import LayoutEyedropperTool from "@/../assets/24px-two-tone/layout-eyedropper-tool.svg";
import ParametricTextTool from "@/../assets/24px-two-tone/parametric-text-tool.svg";
import ParametricFillTool from "@/../assets/24px-two-tone/parametric-fill-tool.svg";
import ParametricGradientTool from "@/../assets/24px-two-tone/parametric-gradient-tool.svg";
import RasterBrushTool from "@/../assets/24px-two-tone/raster-brush-tool.svg";
import RasterHealTool from "@/../assets/24px-two-tone/raster-heal-tool.svg";
import RasterCloneTool from "@/../assets/24px-two-tone/raster-clone-tool.svg";
import RasterPatchTool from "@/../assets/24px-two-tone/raster-patch-tool.svg";
import RasterBlurSharpenTool from "@/../assets/24px-two-tone/raster-detail-tool.svg";
import RasterRelightTool from "@/../assets/24px-two-tone/raster-relight-tool.svg";
import VectorPathTool from "@/../assets/24px-two-tone/vector-path-tool.svg";
import VectorPenTool from "@/../assets/24px-two-tone/vector-pen-tool.svg";
import VectorFreehandTool from "@/../assets/24px-two-tone/vector-freehand-tool.svg";
import VectorSplineTool from "@/../assets/24px-two-tone/vector-spline-tool.svg";
import VectorLineTool from "@/../assets/24px-two-tone/vector-line-tool.svg";
import VectorRectangleTool from "@/../assets/24px-two-tone/vector-rectangle-tool.svg";
import VectorEllipseTool from "@/../assets/24px-two-tone/vector-ellipse-tool.svg";
import VectorShapeTool from "@/../assets/24px-two-tone/vector-shape-tool.svg";
import AlignLeft from "@/../assets/16px-solid/align-left.svg";
import AlignHorizontalCenter from "@/../assets/16px-solid/align-horizontal-center.svg";
import AlignRight from "@/../assets/16px-solid/align-right.svg";
import AlignTop from "@/../assets/16px-solid/align-top.svg";
import AlignVerticalCenter from "@/../assets/16px-solid/align-vertical-center.svg";
import AlignBottom from "@/../assets/16px-solid/align-bottom.svg";
import FlipHorizontal from "@/../assets/16px-solid/flip-horizontal.svg";
import FlipVertical from "@/../assets/16px-solid/flip-vertical.svg";
import BooleanUnion from "@/../assets/16px-solid/boolean-union.svg";
import BooleanSubtractFront from "@/../assets/16px-solid/boolean-subtract-front.svg";
import BooleanSubtractBack from "@/../assets/16px-solid/boolean-subtract-back.svg";
import BooleanIntersect from "@/../assets/16px-solid/boolean-intersect.svg";
import BooleanDifference from "@/../assets/16px-solid/boolean-difference.svg";
import ZoomReset from "@/../assets/16px-solid/zoom-reset.svg";
import ZoomIn from "@/../assets/16px-solid/zoom-in.svg";
import ZoomOut from "@/../assets/16px-solid/zoom-out.svg";
import ViewModeNormal from "@/../assets/16px-solid/view-mode-normal.svg";
import ViewModeOutline from "@/../assets/16px-solid/view-mode-outline.svg";
import ViewModePixels from "@/../assets/16px-solid/view-mode-pixels.svg";
import EyeVisible from "@/../assets/16px-solid/eye-visible.svg";
import EyeHidden from "@/../assets/16px-solid/eye-hidden.svg";
import GraphiteLogo from "@/../assets/16px-solid/graphite-logo.svg";
import File from "@/../assets/16px-solid/file.svg";
import Copy from "@/../assets/16px-solid/copy.svg";
import Paste from "@/../assets/16px-solid/paste.svg";
import ViewportDesignMode from "@/../assets/16px-solid/viewport-design-mode.svg";
import ViewportSelectMode from "@/../assets/16px-solid/viewport-select-mode.svg";
import ViewportGuideMode from "@/../assets/16px-solid/viewport-guide-mode.svg";
import { DefineComponent, defineComponent, PropType } from "vue";
import Checkmark from "@/../assets/12px-solid/checkmark.svg";
import Link from "@/../assets/12px-solid/link.svg";
import Grid from "@/../assets/12px-solid/grid.svg";
import Overlays from "@/../assets/12px-solid/overlays.svg";
import Snapping from "@/../assets/12px-solid/snapping.svg";
import Info from "@/../assets/12px-solid/info.svg";
import Warning from "@/../assets/12px-solid/warning.svg";
import Swap from "@/../assets/12px-solid/swap.svg";
import ResetColors from "@/../assets/12px-solid/reset-colors.svg";
import DropdownArrow from "@/../assets/12px-solid/dropdown-arrow.svg";
import VerticalEllipsis from "@/../assets/12px-solid/vertical-ellipsis.svg";
import CloseX from "@/../assets/12px-solid/close-x.svg";
import DropdownArrow from "@/../assets/12px-solid/dropdown-arrow.svg";
import FullscreenEnter from "@/../assets/12px-solid/fullscreen-enter.svg";
import FullscreenExit from "@/../assets/12px-solid/fullscreen-exit.svg";
import WindowButtonWinMinimize from "@/../assets/12px-solid/window-button-win-minimize.svg";
import WindowButtonWinMaximize from "@/../assets/12px-solid/window-button-win-maximize.svg";
import WindowButtonWinRestoreDown from "@/../assets/12px-solid/window-button-win-restore-down.svg";
import WindowButtonWinClose from "@/../assets/12px-solid/window-button-win-close.svg";
import KeyboardArrowUp from "@/../assets/12px-solid/keyboard-arrow-up.svg";
import KeyboardArrowRight from "@/../assets/12px-solid/keyboard-arrow-right.svg";
import Grid from "@/../assets/12px-solid/grid.svg";
import Info from "@/../assets/12px-solid/info.svg";
import KeyboardArrowDown from "@/../assets/12px-solid/keyboard-arrow-down.svg";
import KeyboardArrowLeft from "@/../assets/12px-solid/keyboard-arrow-left.svg";
import KeyboardArrowRight from "@/../assets/12px-solid/keyboard-arrow-right.svg";
import KeyboardArrowUp from "@/../assets/12px-solid/keyboard-arrow-up.svg";
import KeyboardBackspace from "@/../assets/12px-solid/keyboard-backspace.svg";
import KeyboardCommand from "@/../assets/12px-solid/keyboard-command.svg";
import KeyboardEnter from "@/../assets/12px-solid/keyboard-enter.svg";
@@ -110,124 +48,199 @@ import KeyboardOption from "@/../assets/12px-solid/keyboard-option.svg";
import KeyboardShift from "@/../assets/12px-solid/keyboard-shift.svg";
import KeyboardSpace from "@/../assets/12px-solid/keyboard-space.svg";
import KeyboardTab from "@/../assets/12px-solid/keyboard-tab.svg";
import Link from "@/../assets/12px-solid/link.svg";
import Overlays from "@/../assets/12px-solid/overlays.svg";
import ResetColors from "@/../assets/12px-solid/reset-colors.svg";
import Snapping from "@/../assets/12px-solid/snapping.svg";
import Swap from "@/../assets/12px-solid/swap.svg";
import VerticalEllipsis from "@/../assets/12px-solid/vertical-ellipsis.svg";
import Warning from "@/../assets/12px-solid/warning.svg";
import WindowButtonWinClose from "@/../assets/12px-solid/window-button-win-close.svg";
import WindowButtonWinMaximize from "@/../assets/12px-solid/window-button-win-maximize.svg";
import WindowButtonWinMinimize from "@/../assets/12px-solid/window-button-win-minimize.svg";
import WindowButtonWinRestoreDown from "@/../assets/12px-solid/window-button-win-restore-down.svg";
import AlignBottom from "@/../assets/16px-solid/align-bottom.svg";
import AlignHorizontalCenter from "@/../assets/16px-solid/align-horizontal-center.svg";
import AlignLeft from "@/../assets/16px-solid/align-left.svg";
import AlignRight from "@/../assets/16px-solid/align-right.svg";
import AlignTop from "@/../assets/16px-solid/align-top.svg";
import AlignVerticalCenter from "@/../assets/16px-solid/align-vertical-center.svg";
import BooleanDifference from "@/../assets/16px-solid/boolean-difference.svg";
import BooleanIntersect from "@/../assets/16px-solid/boolean-intersect.svg";
import BooleanSubtractBack from "@/../assets/16px-solid/boolean-subtract-back.svg";
import BooleanSubtractFront from "@/../assets/16px-solid/boolean-subtract-front.svg";
import BooleanUnion from "@/../assets/16px-solid/boolean-union.svg";
import Copy from "@/../assets/16px-solid/copy.svg";
import EyeHidden from "@/../assets/16px-solid/eye-hidden.svg";
import EyeVisible from "@/../assets/16px-solid/eye-visible.svg";
import File from "@/../assets/16px-solid/file.svg";
import FlipHorizontal from "@/../assets/16px-solid/flip-horizontal.svg";
import FlipVertical from "@/../assets/16px-solid/flip-vertical.svg";
import GraphiteLogo from "@/../assets/16px-solid/graphite-logo.svg";
import Paste from "@/../assets/16px-solid/paste.svg";
import ViewModeNormal from "@/../assets/16px-solid/view-mode-normal.svg";
import ViewModeOutline from "@/../assets/16px-solid/view-mode-outline.svg";
import ViewModePixels from "@/../assets/16px-solid/view-mode-pixels.svg";
import ViewportDesignMode from "@/../assets/16px-solid/viewport-design-mode.svg";
import ViewportGuideMode from "@/../assets/16px-solid/viewport-guide-mode.svg";
import ViewportSelectMode from "@/../assets/16px-solid/viewport-select-mode.svg";
import ZoomIn from "@/../assets/16px-solid/zoom-in.svg";
import ZoomOut from "@/../assets/16px-solid/zoom-out.svg";
import ZoomReset from "@/../assets/16px-solid/zoom-reset.svg";
import MouseHintNone from "@/../assets/16px-two-tone/mouse-hint-none.svg";
import MouseHintLmb from "@/../assets/16px-two-tone/mouse-hint-lmb.svg";
import MouseHintRmb from "@/../assets/16px-two-tone/mouse-hint-rmb.svg";
import MouseHintMmb from "@/../assets/16px-two-tone/mouse-hint-mmb.svg";
import MouseHintScrollUp from "@/../assets/16px-two-tone/mouse-hint-scroll-up.svg";
import MouseHintScrollDown from "@/../assets/16px-two-tone/mouse-hint-scroll-down.svg";
import MouseHintDrag from "@/../assets/16px-two-tone/mouse-hint-drag.svg";
import MouseHintLmbDrag from "@/../assets/16px-two-tone/mouse-hint-lmb-drag.svg";
import MouseHintRmbDrag from "@/../assets/16px-two-tone/mouse-hint-rmb-drag.svg";
import MouseHintLmb from "@/../assets/16px-two-tone/mouse-hint-lmb.svg";
import MouseHintMmbDrag from "@/../assets/16px-two-tone/mouse-hint-mmb-drag.svg";
import MouseHintMmb from "@/../assets/16px-two-tone/mouse-hint-mmb.svg";
import MouseHintNone from "@/../assets/16px-two-tone/mouse-hint-none.svg";
import MouseHintRmbDrag from "@/../assets/16px-two-tone/mouse-hint-rmb-drag.svg";
import MouseHintRmb from "@/../assets/16px-two-tone/mouse-hint-rmb.svg";
import MouseHintScrollDown from "@/../assets/16px-two-tone/mouse-hint-scroll-down.svg";
import MouseHintScrollUp from "@/../assets/16px-two-tone/mouse-hint-scroll-up.svg";
import NodeTypePath from "@/../assets/24px-full-color/node-type-path.svg";
import NodeTypeFolder from "@/../assets/24px-full-color/node-type-folder.svg";
import NodeTypePath from "@/../assets/24px-full-color/node-type-path.svg";
const icons = {
LayoutSelectTool: { component: LayoutSelectTool, size: 24 },
LayoutCropTool: { component: LayoutCropTool, size: 24 },
LayoutNavigateTool: { component: LayoutNavigateTool, size: 24 },
LayoutEyedropperTool: { component: LayoutEyedropperTool, size: 24 },
ParametricTextTool: { component: ParametricTextTool, size: 24 },
ParametricFillTool: { component: ParametricFillTool, size: 24 },
ParametricGradientTool: { component: ParametricGradientTool, size: 24 },
RasterBrushTool: { component: RasterBrushTool, size: 24 },
RasterHealTool: { component: RasterHealTool, size: 24 },
RasterCloneTool: { component: RasterCloneTool, size: 24 },
RasterPatchTool: { component: RasterPatchTool, size: 24 },
RasterBlurSharpenTool: { component: RasterBlurSharpenTool, size: 24 },
RasterRelightTool: { component: RasterRelightTool, size: 24 },
VectorPathTool: { component: VectorPathTool, size: 24 },
VectorPenTool: { component: VectorPenTool, size: 24 },
VectorFreehandTool: { component: VectorFreehandTool, size: 24 },
VectorSplineTool: { component: VectorSplineTool, size: 24 },
VectorLineTool: { component: VectorLineTool, size: 24 },
VectorRectangleTool: { component: VectorRectangleTool, size: 24 },
VectorEllipseTool: { component: VectorEllipseTool, size: 24 },
VectorShapeTool: { component: VectorShapeTool, size: 24 },
AlignLeft: { component: AlignLeft, size: 16 },
AlignHorizontalCenter: { component: AlignHorizontalCenter, size: 16 },
AlignRight: { component: AlignRight, size: 16 },
AlignTop: { component: AlignTop, size: 16 },
AlignVerticalCenter: { component: AlignVerticalCenter, size: 16 },
AlignBottom: { component: AlignBottom, size: 16 },
FlipHorizontal: { component: FlipHorizontal, size: 16 },
FlipVertical: { component: FlipVertical, size: 16 },
BooleanUnion: { component: BooleanUnion, size: 16 },
BooleanSubtractFront: { component: BooleanSubtractFront, size: 16 },
BooleanSubtractBack: { component: BooleanSubtractBack, size: 16 },
BooleanIntersect: { component: BooleanIntersect, size: 16 },
BooleanDifference: { component: BooleanDifference, size: 16 },
ZoomReset: { component: ZoomReset, size: 16 },
ZoomIn: { component: ZoomIn, size: 16 },
ZoomOut: { component: ZoomOut, size: 16 },
ViewModeNormal: { component: ViewModeNormal, size: 16 },
ViewModeOutline: { component: ViewModeOutline, size: 16 },
ViewModePixels: { component: ViewModePixels, size: 16 },
EyeVisible: { component: EyeVisible, size: 16 },
EyeHidden: { component: EyeHidden, size: 16 },
GraphiteLogo: { component: GraphiteLogo, size: 16 },
File: { component: File, size: 16 },
Copy: { component: Copy, size: 16 },
Paste: { component: Paste, size: 16 },
ViewportDesignMode: { component: ViewportDesignMode, size: 16 },
ViewportSelectMode: { component: ViewportSelectMode, size: 16 },
ViewportGuideMode: { component: ViewportGuideMode, size: 16 },
Checkmark: { component: Checkmark, size: 12 },
Link: { component: Link, size: 12 },
Grid: { component: Grid, size: 12 },
Overlays: { component: Overlays, size: 12 },
Snapping: { component: Snapping, size: 12 },
Info: { component: Info, size: 12 },
Warning: { component: Warning, size: 12 },
Swap: { component: Swap, size: 12 },
ResetColors: { component: ResetColors, size: 12 },
DropdownArrow: { component: DropdownArrow, size: 12 },
VerticalEllipsis: { component: VerticalEllipsis, size: 12 },
CloseX: { component: CloseX, size: 12 },
FullscreenEnter: { component: FullscreenEnter, size: 12 },
FullscreenExit: { component: FullscreenExit, size: 12 },
WindowButtonWinMinimize: { component: WindowButtonWinMinimize, size: 12 },
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: 12 },
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: 12 },
WindowButtonWinClose: { component: WindowButtonWinClose, size: 12 },
KeyboardArrowUp: { component: KeyboardArrowUp, size: 12 },
KeyboardArrowRight: { component: KeyboardArrowRight, size: 12 },
KeyboardArrowDown: { component: KeyboardArrowDown, size: 12 },
KeyboardArrowLeft: { component: KeyboardArrowLeft, size: 12 },
KeyboardBackspace: { component: KeyboardBackspace, size: 12 },
KeyboardCommand: { component: KeyboardCommand, size: 12 },
KeyboardEnter: { component: KeyboardEnter, size: 12 },
KeyboardOption: { component: KeyboardOption, size: 12 },
KeyboardShift: { component: KeyboardShift, size: 12 },
KeyboardSpace: { component: KeyboardSpace, size: 12 },
KeyboardTab: { component: KeyboardTab, size: 12 },
MouseHintNone: { component: MouseHintNone, size: 16 },
MouseHintLmb: { component: MouseHintLmb, size: 16 },
MouseHintRmb: { component: MouseHintRmb, size: 16 },
MouseHintMmb: { component: MouseHintMmb, size: 16 },
MouseHintScrollUp: { component: MouseHintScrollUp, size: 16 },
MouseHintScrollDown: { component: MouseHintScrollDown, size: 16 },
MouseHintDrag: { component: MouseHintDrag, size: 16 },
MouseHintLmbDrag: { component: MouseHintLmbDrag, size: 16 },
MouseHintRmbDrag: { component: MouseHintRmbDrag, size: 16 },
MouseHintMmbDrag: { component: MouseHintMmbDrag, size: 16 },
NodeTypePath: { component: NodeTypePath, size: 24 },
NodeTypeFolder: { component: NodeTypeFolder, size: 24 },
import LayoutCropTool from "@/../assets/24px-two-tone/layout-crop-tool.svg";
import LayoutEyedropperTool from "@/../assets/24px-two-tone/layout-eyedropper-tool.svg";
import LayoutNavigateTool from "@/../assets/24px-two-tone/layout-navigate-tool.svg";
import LayoutSelectTool from "@/../assets/24px-two-tone/layout-select-tool.svg";
import ParametricFillTool from "@/../assets/24px-two-tone/parametric-fill-tool.svg";
import ParametricGradientTool from "@/../assets/24px-two-tone/parametric-gradient-tool.svg";
import ParametricTextTool from "@/../assets/24px-two-tone/parametric-text-tool.svg";
import RasterBrushTool from "@/../assets/24px-two-tone/raster-brush-tool.svg";
import RasterCloneTool from "@/../assets/24px-two-tone/raster-clone-tool.svg";
import RasterBlurSharpenTool from "@/../assets/24px-two-tone/raster-detail-tool.svg";
import RasterHealTool from "@/../assets/24px-two-tone/raster-heal-tool.svg";
import RasterPatchTool from "@/../assets/24px-two-tone/raster-patch-tool.svg";
import RasterRelightTool from "@/../assets/24px-two-tone/raster-relight-tool.svg";
import VectorEllipseTool from "@/../assets/24px-two-tone/vector-ellipse-tool.svg";
import VectorFreehandTool from "@/../assets/24px-two-tone/vector-freehand-tool.svg";
import VectorLineTool from "@/../assets/24px-two-tone/vector-line-tool.svg";
import VectorPathTool from "@/../assets/24px-two-tone/vector-path-tool.svg";
import VectorPenTool from "@/../assets/24px-two-tone/vector-pen-tool.svg";
import VectorRectangleTool from "@/../assets/24px-two-tone/vector-rectangle-tool.svg";
import VectorShapeTool from "@/../assets/24px-two-tone/vector-shape-tool.svg";
import VectorSplineTool from "@/../assets/24px-two-tone/vector-spline-tool.svg";
export type IconName = keyof typeof ICON_LIST;
export type IconSize = 12 | 16 | 24 | 32;
const size12: IconSize = 12;
const size16: IconSize = 16;
const size24: IconSize = 24;
// const size32: IconSize = 32;
const ICON_LIST = {
Checkmark: { component: Checkmark, size: size12 },
CloseX: { component: CloseX, size: size12 },
DropdownArrow: { component: DropdownArrow, size: size12 },
FullscreenEnter: { component: FullscreenEnter, size: size12 },
FullscreenExit: { component: FullscreenExit, size: size12 },
Grid: { component: Grid, size: size12 },
Info: { component: Info, size: size12 },
KeyboardArrowDown: { component: KeyboardArrowDown, size: size12 },
KeyboardArrowLeft: { component: KeyboardArrowLeft, size: size12 },
KeyboardArrowRight: { component: KeyboardArrowRight, size: size12 },
KeyboardArrowUp: { component: KeyboardArrowUp, size: size12 },
KeyboardBackspace: { component: KeyboardBackspace, size: size12 },
KeyboardCommand: { component: KeyboardCommand, size: size12 },
KeyboardEnter: { component: KeyboardEnter, size: size12 },
KeyboardOption: { component: KeyboardOption, size: size12 },
KeyboardShift: { component: KeyboardShift, size: size12 },
KeyboardSpace: { component: KeyboardSpace, size: size12 },
KeyboardTab: { component: KeyboardTab, size: size12 },
Link: { component: Link, size: size12 },
Overlays: { component: Overlays, size: size12 },
ResetColors: { component: ResetColors, size: size12 },
Snapping: { component: Snapping, size: size12 },
Swap: { component: Swap, size: size12 },
VerticalEllipsis: { component: VerticalEllipsis, size: size12 },
Warning: { component: Warning, size: size12 },
WindowButtonWinClose: { component: WindowButtonWinClose, size: size12 },
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: size12 },
WindowButtonWinMinimize: { component: WindowButtonWinMinimize, size: size12 },
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: size12 },
AlignBottom: { component: AlignBottom, size: size16 },
AlignHorizontalCenter: { component: AlignHorizontalCenter, size: size16 },
AlignLeft: { component: AlignLeft, size: size16 },
AlignRight: { component: AlignRight, size: size16 },
AlignTop: { component: AlignTop, size: size16 },
AlignVerticalCenter: { component: AlignVerticalCenter, size: size16 },
BooleanDifference: { component: BooleanDifference, size: size16 },
BooleanIntersect: { component: BooleanIntersect, size: size16 },
BooleanSubtractBack: { component: BooleanSubtractBack, size: size16 },
BooleanSubtractFront: { component: BooleanSubtractFront, size: size16 },
BooleanUnion: { component: BooleanUnion, size: size16 },
Copy: { component: Copy, size: size16 },
EyeHidden: { component: EyeHidden, size: size16 },
EyeVisible: { component: EyeVisible, size: size16 },
File: { component: File, size: size16 },
FlipHorizontal: { component: FlipHorizontal, size: size16 },
FlipVertical: { component: FlipVertical, size: size16 },
GraphiteLogo: { component: GraphiteLogo, size: size16 },
Paste: { component: Paste, size: size16 },
ViewModeNormal: { component: ViewModeNormal, size: size16 },
ViewModeOutline: { component: ViewModeOutline, size: size16 },
ViewModePixels: { component: ViewModePixels, size: size16 },
ViewportDesignMode: { component: ViewportDesignMode, size: size16 },
ViewportGuideMode: { component: ViewportGuideMode, size: size16 },
ViewportSelectMode: { component: ViewportSelectMode, size: size16 },
ZoomIn: { component: ZoomIn, size: size16 },
ZoomOut: { component: ZoomOut, size: size16 },
ZoomReset: { component: ZoomReset, size: size16 },
MouseHintDrag: { component: MouseHintDrag, size: size16 },
MouseHintLmbDrag: { component: MouseHintLmbDrag, size: size16 },
MouseHintLmb: { component: MouseHintLmb, size: size16 },
MouseHintMmbDrag: { component: MouseHintMmbDrag, size: size16 },
MouseHintMmb: { component: MouseHintMmb, size: size16 },
MouseHintNone: { component: MouseHintNone, size: size16 },
MouseHintRmbDrag: { component: MouseHintRmbDrag, size: size16 },
MouseHintRmb: { component: MouseHintRmb, size: size16 },
MouseHintScrollDown: { component: MouseHintScrollDown, size: size16 },
MouseHintScrollUp: { component: MouseHintScrollUp, size: size16 },
NodeTypeFolder: { component: NodeTypeFolder, size: size24 },
NodeTypePath: { component: NodeTypePath, size: size24 },
LayoutCropTool: { component: LayoutCropTool, size: size24 },
LayoutEyedropperTool: { component: LayoutEyedropperTool, size: size24 },
LayoutNavigateTool: { component: LayoutNavigateTool, size: size24 },
LayoutSelectTool: { component: LayoutSelectTool, size: size24 },
ParametricFillTool: { component: ParametricFillTool, size: size24 },
ParametricGradientTool: { component: ParametricGradientTool, size: size24 },
ParametricTextTool: { component: ParametricTextTool, size: size24 },
RasterBrushTool: { component: RasterBrushTool, size: size24 },
RasterCloneTool: { component: RasterCloneTool, size: size24 },
RasterBlurSharpenTool: { component: RasterBlurSharpenTool, size: size24 },
RasterHealTool: { component: RasterHealTool, size: size24 },
RasterPatchTool: { component: RasterPatchTool, size: size24 },
RasterRelightTool: { component: RasterRelightTool, size: size24 },
VectorEllipseTool: { component: VectorEllipseTool, size: size24 },
VectorFreehandTool: { component: VectorFreehandTool, size: size24 },
VectorLineTool: { component: VectorLineTool, size: size24 },
VectorPathTool: { component: VectorPathTool, size: size24 },
VectorPenTool: { component: VectorPenTool, size: size24 },
VectorRectangleTool: { component: VectorRectangleTool, size: size24 },
VectorShapeTool: { component: VectorShapeTool, size: size24 },
VectorSplineTool: { component: VectorSplineTool, size: size24 },
};
const components = Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component]));
const icons: Record<IconName, { component: DefineComponent; size: IconSize }> = ICON_LIST;
export default defineComponent({
components: Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component])),
props: {
icon: { type: String, required: true },
gapAfter: { type: Boolean, default: false },
icon: { type: String as PropType<IconName>, required: true },
gapAfter: { type: Boolean as PropType<boolean>, default: false },
},
components,
data() {
return { icons };
return {
icons,
};
},
});
</script>
@@ -20,13 +20,12 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
export default defineComponent({
components: {},
props: {
bold: { type: Boolean, default: false },
italic: { type: Boolean, default: false },
bold: { type: Boolean as PropType<boolean>, default: false },
italic: { type: Boolean as PropType<boolean>, default: false },
},
});
</script>
@@ -10,7 +10,7 @@
</template>
</template>
<span class="input-mouse" v-if="inputMouse">
<IconLabel :icon="mouseMovementIcon(inputMouse)" />
<IconLabel :icon="`MouseHint${inputMouse}`" />
</span>
<span class="hint-text" v-if="hasSlotContent">
<slot></slot>
@@ -95,28 +95,17 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import { HintInfo } from "@/dispatcher/js-messages";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
export enum MouseInputInteraction {
"None" = "None",
"Lmb" = "Lmb",
"Rmb" = "Rmb",
"Mmb" = "Mmb",
"ScrollUp" = "ScrollUp",
"ScrollDown" = "ScrollDown",
"Drag" = "Drag",
"LmbDrag" = "LmbDrag",
"RmbDrag" = "RmbDrag",
"MmbDrag" = "MmbDrag",
}
export default defineComponent({
components: { IconLabel },
props: {
inputKeys: { type: Array, default: () => [] },
inputMouse: { type: String },
inputKeys: { type: Array as PropType<HintInfo["key_groups"]>, default: () => [] },
inputMouse: { type: String as PropType<HintInfo["mouse"]>, default: null },
},
computed: {
hasSlotContent(): boolean {
@@ -173,50 +162,14 @@ export default defineComponent({
let result;
// Letters and numbers
if (/^[A-Z0-9]$/.test(text)) {
result = text;
}
if (/^[A-Z0-9]$/.test(text)) result = text;
// Abbreviated names
else if (text in textMap) {
result = textMap[text];
}
else if (text in textMap) result = textMap[text];
// Other
else {
result = text;
}
else result = text;
return { text: result, icon: null, width: `width-${(result || " ").length * 8 + 8}` };
},
mouseMovementIcon(mouseInputInteraction: MouseInputInteraction) {
switch (mouseInputInteraction) {
case MouseInputInteraction.Lmb:
return "MouseHintLmb";
case MouseInputInteraction.Rmb:
return "MouseHintRmb";
case MouseInputInteraction.Mmb:
return "MouseHintMmb";
case MouseInputInteraction.ScrollUp:
return "MouseHintScrollUp";
case MouseInputInteraction.ScrollDown:
return "MouseHintScrollDown";
case MouseInputInteraction.Drag:
return "MouseHintDrag";
case MouseInputInteraction.LmbDrag:
return "MouseHintLmbDrag";
case MouseInputInteraction.RmbDrag:
return "MouseHintRmbDrag";
case MouseInputInteraction.MmbDrag:
return "MouseHintMmbDrag";
default:
case MouseInputInteraction.None:
return "MouseHintNone";
}
},
},
data() {
return {
MouseInputInteraction,
};
},
});
</script>
@@ -31,17 +31,19 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { WidgetRow, SeparatorType, IconButtonWidget } from "@/components/widgets/widgets";
import { WidgetRow, IconButtonWidget } from "@/utilities/widgets";
import Separator from "@/components/widgets/separators/Separator.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
export type ToolName = "Select" | "Shape" | "Line" | "Pen";
export default defineComponent({
inject: ["editor", "dialog"],
props: {
activeTool: { type: String },
activeTool: { type: String as PropType<ToolName> },
activeToolOptions: { type: Object as PropType<Record<string, object>> },
},
methods: {
@@ -54,11 +56,15 @@ export default defineComponent({
},
// Traverses the given path and returns the direct parent of the option
getRecordContainingOption(optionPath: string[]): Record<string, number> {
const allButLast = optionPath.slice(0, -1);
// TODO: Formalize types and avoid casting with `as`
let currentRecord = this.activeToolOptions as Record<string, object | number>;
[this.activeTool || "", ...allButLast].forEach((attr) => {
const allButLastOptions = optionPath.slice(0, -1);
[this.activeTool || "", ...allButLastOptions].forEach((attr) => {
// Dig into the tree in each loop iteration
currentRecord = currentRecord[attr] as Record<string, object | number>;
});
return currentRecord as Record<string, number>;
},
// Traverses the given path into the active tool's option struct, and sets the value at the path tail
@@ -88,19 +94,19 @@ export default defineComponent({
},
},
data() {
const toolOptionsWidgets: Record<string, WidgetRow> = {
const toolOptionsWidgets: Record<ToolName, WidgetRow> = {
Select: [
{ kind: "IconButton", message: { Align: ["X", "Min"] }, tooltip: "Align Left", props: { icon: "AlignLeft", size: 24 } },
{ kind: "IconButton", message: { Align: ["X", "Center"] }, tooltip: "Align Horizontal Center", props: { icon: "AlignHorizontalCenter", size: 24 } },
{ kind: "IconButton", message: { Align: ["X", "Max"] }, tooltip: "Align Right", props: { icon: "AlignRight", size: 24 } },
{ kind: "Separator", props: { type: SeparatorType.Unrelated } },
{ kind: "Separator", props: { type: "Unrelated" } },
{ kind: "IconButton", message: { Align: ["Y", "Min"] }, tooltip: "Align Top", props: { icon: "AlignTop", size: 24 } },
{ kind: "IconButton", message: { Align: ["Y", "Center"] }, tooltip: "Align Vertical Center", props: { icon: "AlignVerticalCenter", size: 24 } },
{ kind: "IconButton", message: { Align: ["Y", "Max"] }, tooltip: "Align Bottom", props: { icon: "AlignBottom", size: 24 } },
{ kind: "Separator", props: { type: SeparatorType.Related } },
{ kind: "Separator", props: { type: "Related" } },
{
kind: "PopoverButton",
@@ -111,12 +117,12 @@ export default defineComponent({
props: {},
},
{ kind: "Separator", props: { type: SeparatorType.Section } },
{ kind: "Separator", props: { type: "Section" } },
{ kind: "IconButton", message: "FlipHorizontal", tooltip: "Flip Horizontal", props: { icon: "FlipHorizontal", size: 24 } },
{ kind: "IconButton", message: "FlipVertical", tooltip: "Flip Vertical", props: { icon: "FlipVertical", size: 24 } },
{ kind: "Separator", props: { type: SeparatorType.Related } },
{ kind: "Separator", props: { type: "Related" } },
{
kind: "PopoverButton",
@@ -127,15 +133,15 @@ export default defineComponent({
props: {},
},
{ kind: "Separator", props: { type: SeparatorType.Section } },
{ kind: "Separator", props: { type: "Section" } },
{ kind: "IconButton", tooltip: "Boolean Union", callback: () => this.dialog.comingSoon(197), props: { icon: "BooleanUnion", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Front", callback: () => this.dialog.comingSoon(197), props: { icon: "BooleanSubtractFront", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Back", callback: () => this.dialog.comingSoon(197), props: { icon: "BooleanSubtractBack", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Intersect", callback: () => this.dialog.comingSoon(197), props: { icon: "BooleanIntersect", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Difference", callback: () => this.dialog.comingSoon(197), props: { icon: "BooleanDifference", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Union", callback: (): void => this.dialog.comingSoon(197), props: { icon: "BooleanUnion", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Front", callback: (): void => this.dialog.comingSoon(197), props: { icon: "BooleanSubtractFront", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Back", callback: (): void => this.dialog.comingSoon(197), props: { icon: "BooleanSubtractBack", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Intersect", callback: (): void => this.dialog.comingSoon(197), props: { icon: "BooleanIntersect", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Difference", callback: (): void => this.dialog.comingSoon(197), props: { icon: "BooleanDifference", size: 24 } },
{ kind: "Separator", props: { type: SeparatorType.Related } },
{ kind: "Separator", props: { type: "Related" } },
{
kind: "PopoverButton",
@@ -153,7 +159,6 @@ export default defineComponent({
return {
toolOptionsWidgets,
SeparatorType,
};
},
components: {
@@ -50,29 +50,26 @@ const MAJOR_MARK_THICKNESS = 16;
const MEDIUM_MARK_THICKNESS = 6;
const MINOR_MARK_THICKNESS = 3;
export enum RulerDirection {
"Horizontal" = "Horizontal",
"Vertical" = "Vertical",
}
export type RulerDirection = "Horizontal" | "Vertical";
// Apparently the modulo operator in js does not work properly.
const mod = (n: number, m: number) => {
const remain = n % m;
return Math.floor(remain >= 0 ? remain : remain + m);
const mod = (n: number, m: number): number => {
const remainder = n % m;
return Math.floor(remainder >= 0 ? remainder : remainder + m);
};
export default defineComponent({
props: {
direction: { type: String as PropType<RulerDirection>, default: RulerDirection.Vertical },
origin: { type: Number, required: true },
numberInterval: { type: Number, required: true },
majorMarkSpacing: { type: Number, required: true },
mediumDivisions: { type: Number, default: 5 },
minorDivisions: { type: Number, default: 2 },
direction: { type: String as PropType<RulerDirection>, default: "Vertical" },
origin: { type: Number as PropType<number>, required: true },
numberInterval: { type: Number as PropType<number>, required: true },
majorMarkSpacing: { type: Number as PropType<number>, required: true },
mediumDivisions: { type: Number as PropType<number>, default: 5 },
minorDivisions: { type: Number as PropType<number>, default: 2 },
},
computed: {
svgPath(): string {
const isVertical = this.direction === RulerDirection.Vertical;
const isVertical = this.direction === "Vertical";
const lineDirection = isVertical ? "H" : "V";
const offsetStart = mod(this.origin, this.majorMarkSpacing);
@@ -98,7 +95,7 @@ export default defineComponent({
return dPathAttribute;
},
svgTexts(): { transform: string; text: number }[] {
const isVertical = this.direction === RulerDirection.Vertical;
const isVertical = this.direction === "Vertical";
const offsetStart = mod(this.origin, this.majorMarkSpacing);
const shiftedOffsetStart = offsetStart - this.majorMarkSpacing;
@@ -128,7 +125,7 @@ export default defineComponent({
if (!this.$refs.rulerRef) return;
const rulerElement = this.$refs.rulerRef as HTMLElement;
const isVertical = this.direction === RulerDirection.Vertical;
const isVertical = this.direction === "Vertical";
const newLength = isVertical ? rulerElement.clientHeight : rulerElement.clientWidth;
const roundedUp = (Math.floor(newLength / this.majorMarkSpacing) + 1) * this.majorMarkSpacing;
@@ -152,7 +149,6 @@ export default defineComponent({
return {
rulerLength: 0,
svgBounds: { width: "0px", height: "0px" },
RulerDirection,
};
},
});
@@ -1,8 +1,8 @@
<template>
<div class="persistent-scrollbar" :class="direction.toLowerCase()">
<button class="arrow decrease" @pointerdown="changePosition(-50)"></button>
<div class="scroll-track" ref="scrollTrack" @pointerdown="grabArea">
<div class="scroll-thumb" @pointerdown="grabHandle" :class="{ dragging }" ref="handle" :style="[thumbStart, thumbEnd, sides]"></div>
<div class="scroll-track" ref="scrollTrack" @pointerdown="(e) => grabArea(e)">
<div class="scroll-thumb" @pointerdown="(e) => grabHandle(e)" :class="{ dragging }" ref="handle" :style="[thumbStart, thumbEnd, sides]"></div>
</div>
<button class="arrow increase" @click="changePosition(50)"></button>
</div>
@@ -110,44 +110,40 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
export type ScrollbarDirection = "Horizontal" | "Vertical";
// Linear Interpolation
const lerp = (x: number, y: number, a: number) => x * (1 - a) + y * a;
const lerp = (x: number, y: number, a: number): number => x * (1 - a) + y * a;
// Convert the position of the handle (0-1) to the position on the track (0-1).
// This includes the 1/2 handle length gap of the possible handle positionson each side so the end of the handle doesn't go off the track.
const handleToTrack = (handleLen: number, handlePos: number) => lerp(handleLen / 2, 1 - handleLen / 2, handlePos);
const handleToTrack = (handleLen: number, handlePos: number): number => lerp(handleLen / 2, 1 - handleLen / 2, handlePos);
const pointerPosition = (direction: ScrollbarDirection, e: PointerEvent) => (direction === ScrollbarDirection.Vertical ? e.clientY : e.clientX);
export enum ScrollbarDirection {
"Horizontal" = "Horizontal",
"Vertical" = "Vertical",
}
const pointerPosition = (direction: ScrollbarDirection, e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX);
export default defineComponent({
props: {
direction: { type: String as PropType<ScrollbarDirection>, default: ScrollbarDirection.Vertical },
handlePosition: { type: Number, default: 0.5 },
handleLength: { type: Number, default: 0.5 },
direction: { type: String as PropType<ScrollbarDirection>, default: "Vertical" },
handlePosition: { type: Number as PropType<number>, default: 0.5 },
handleLength: { type: Number as PropType<number>, default: 0.5 },
},
computed: {
thumbStart(): { left: string } | { top: string } {
const start = handleToTrack(this.handleLength, this.handlePosition) - this.handleLength / 2;
return this.direction === ScrollbarDirection.Vertical ? { top: `${start * 100}%` } : { left: `${start * 100}%` };
return this.direction === "Vertical" ? { top: `${start * 100}%` } : { left: `${start * 100}%` };
},
thumbEnd(): { right: string } | { bottom: string } {
const end = 1 - handleToTrack(this.handleLength, this.handlePosition) - this.handleLength / 2;
return this.direction === ScrollbarDirection.Vertical ? { bottom: `${end * 100}%` } : { right: `${end * 100}%` };
return this.direction === "Vertical" ? { bottom: `${end * 100}%` } : { right: `${end * 100}%` };
},
sides(): { left: string; right: string } | { top: string; bottom: string } {
return this.direction === ScrollbarDirection.Vertical ? { left: "0%", right: "0%" } : { top: "0%", bottom: "0%" };
return this.direction === "Vertical" ? { left: "0%", right: "0%" } : { top: "0%", bottom: "0%" };
},
},
data() {
return {
ScrollbarDirection,
dragging: false,
pointerPos: 0,
};
@@ -159,11 +155,11 @@ export default defineComponent({
methods: {
trackLength(): number {
const track = this.$refs.scrollTrack as HTMLElement;
return this.direction === ScrollbarDirection.Vertical ? track.clientHeight - this.handleLength : track.clientWidth;
return this.direction === "Vertical" ? track.clientHeight - this.handleLength : track.clientWidth;
},
trackOffset(): number {
const track = this.$refs.scrollTrack as HTMLElement;
return this.direction === ScrollbarDirection.Vertical ? track.getBoundingClientRect().top : track.getBoundingClientRect().left;
return this.direction === "Vertical" ? track.getBoundingClientRect().top : track.getBoundingClientRect().left;
},
clampHandlePosition(newPos: number) {
const clampedPosition = Math.min(Math.max(newPos, 0), 1);
@@ -1,6 +1,6 @@
<template>
<div class="separator" :class="[direction.toLowerCase(), type.toLowerCase()]">
<div v-if="[SeparatorType.Section, SeparatorType.List].includes(type)"></div>
<div v-if="['Section', 'List'].includes(type)"></div>
</div>
</template>
@@ -69,21 +69,14 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
import { SeparatorDirection, SeparatorType } from "@/utilities/widgets";
export default defineComponent({
components: {},
props: {
direction: { type: String, default: SeparatorDirection.Horizontal },
type: { type: String, default: SeparatorType.Unrelated },
},
data() {
return {
SeparatorDirection,
SeparatorType,
};
direction: { type: String as PropType<SeparatorDirection>, default: "Horizontal" },
type: { type: String as PropType<SeparatorType>, default: "Unrelated" },
},
});
</script>
@@ -1,97 +0,0 @@
export type Widgets = TextButtonWidget | IconButtonWidget | SeparatorWidget | PopoverButtonWidget | NumberInputWidget;
export type WidgetRow = Widgets[];
export type WidgetLayout = WidgetRow[];
// Text Button
export interface TextButtonWidget {
kind: "TextButton";
tooltip?: string;
message?: string | object;
callback?: () => void;
props: TextButtonProps;
}
export interface TextButtonProps {
// `action` is used via `IconButtonWidget.callback`
label: string;
emphasized?: boolean;
disabled?: boolean;
minWidth?: number;
gapAfter?: boolean;
}
// Icon Button
export interface IconButtonWidget {
kind: "IconButton";
tooltip?: string;
message?: string | object;
callback?: () => void;
props: IconButtonProps;
}
export interface IconButtonProps {
// `action` is used via `IconButtonWidget.callback`
icon: string;
size: number;
gapAfter?: boolean;
}
// Popover Button
export interface PopoverButtonWidget {
kind: "PopoverButton";
tooltip?: string;
callback?: () => void;
// popover: WidgetLayout;
popover: { title: string; text: string }; // TODO: Replace this with a `WidgetLayout` like above for arbitrary layouts
props: PopoverButtonProps;
}
export interface PopoverButtonProps {
// `action` is used via `PopoverButtonWidget.callback`
icon?: string;
}
// Number Input
export interface NumberInputWidget {
kind: "NumberInput";
tooltip?: string;
optionPath: string[];
props: Omit<NumberInputProps, "value">;
}
export interface NumberInputProps {
value: number;
min?: number;
max?: number;
incrementBehavior?: boolean;
incrementFactor?: number;
isInteger?: boolean;
unit?: string;
unitIsHiddenWhenEditing?: boolean;
displayDecimalPlaces?: number;
label?: string;
disabled?: boolean;
}
// Separator
export interface SeparatorWidget {
kind: "Separator";
props: SeparatorProps;
}
export interface SeparatorProps {
direction?: SeparatorDirection;
type?: SeparatorType;
}
export enum SeparatorDirection {
"Horizontal" = "Horizontal",
"Vertical" = "Vertical",
}
export enum SeparatorType {
"Related" = "Related",
"Unrelated" = "Unrelated",
"Section" = "Section",
"List" = "List",
}