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 6662a9a04f
commit 2c8d70acb4
53 changed files with 842 additions and 946 deletions
+29 -50
View File
@@ -1,44 +1,36 @@
export interface RGB {
r: number;
g: number;
b: number;
a: number;
}
import { HSVA, RGBA } from "@/dispatcher/js-messages";
export interface HSV {
h: number;
s: number;
v: number;
a: number;
}
export function hsvaToRgba(hsva: HSVA): RGBA {
const { h, s, v, a } = hsva;
const hue = h * 6;
const hueIntegerPart = Math.floor(hue);
const hueFractionalPart = hue - hueIntegerPart;
const hueIntegerMod6 = hueIntegerPart % 6;
export function hsvToRgb(hsv: HSV): RGB {
let { h } = hsv;
const { s, v } = hsv;
h *= 6;
const i = Math.floor(h);
const f = h - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
const mod = i % 6;
const r = Math.round([v, q, p, p, t, v][mod]);
const g = Math.round([t, v, v, q, p, p][mod]);
const b = Math.round([p, p, t, v, v, q][mod]);
return { r, g, b, a: hsv.a };
const q = v * (1 - hueFractionalPart * s);
const t = v * (1 - (1 - hueFractionalPart) * s);
const r = Math.round([v, q, p, p, t, v][hueIntegerMod6]);
const g = Math.round([t, v, v, q, p, p][hueIntegerMod6]);
const b = Math.round([p, p, t, v, v, q][hueIntegerMod6]);
return { r, g, b, a };
}
export function rgbToHsv(rgb: RGB) {
const { r, g, b } = rgb;
export function rgbaToHsva(rgba: RGBA): HSVA {
const { r, g, b, a } = rgba;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const s = max === 0 ? 0 : d / max;
const v = max;
let h = 0;
if (max === min) {
h = 0;
} else {
if (max !== min) {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
@@ -53,27 +45,14 @@ export function rgbToHsv(rgb: RGB) {
}
h /= 6;
}
return { h, s, v, a: rgb.a };
return { h, s, v, a };
}
export function rgbToDecimalRgb(rgb: RGB) {
const r = rgb.r / 255;
const g = rgb.g / 255;
const b = rgb.b / 255;
return { r, g, b, a: rgb.a };
}
export function rgbaToDecimalRgba(rgba: RGBA): RGBA {
const r = rgba.r / 255;
const g = rgba.g / 255;
const b = rgba.b / 255;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isRGB(data: any): data is RGB {
if (typeof data !== "object" || data === null) return false;
return (
typeof data.r === "number" &&
!Number.isNaN(data.r) &&
typeof data.g === "number" &&
!Number.isNaN(data.g) &&
typeof data.b === "number" &&
!Number.isNaN(data.b) &&
typeof data.a === "number" &&
!Number.isNaN(data.a)
);
return { r, g, b, a: rgba.a };
}
+2 -2
View File
@@ -1,4 +1,4 @@
export function download(filename: string, fileData: string) {
export function download(filename: string, fileData: string): void {
const type = filename.endsWith(".svg") ? "image/svg+xml;charset=utf-8" : "text/plain;charset=utf-8";
const blob = new Blob([fileData], { type });
const url = URL.createObjectURL(blob);
@@ -11,7 +11,7 @@ export function download(filename: string, fileData: string) {
element.click();
}
export async function upload(acceptedEextensions: string) {
export async function upload(acceptedEextensions: string): Promise<{ filename: string; content: string }> {
return new Promise<{ filename: string; content: string }>((resolve, _) => {
const element = document.createElement("input");
element.type = "file";
+1 -1
View File
@@ -1,3 +1,3 @@
export function clamp(value: number, min = 0, max = 1) {
export function clamp(value: number, min = 0, max = 1): number {
return Math.max(min, Math.min(value, max));
}
+1 -1
View File
@@ -1,4 +1,4 @@
export function stripIndents(stringPieces: TemplateStringsArray, ...substitutions: unknown[]) {
export function stripIndents(stringPieces: TemplateStringsArray, ...substitutions: unknown[]): string {
const interleavedSubstitutions = stringPieces.flatMap((stringPiece, index) => [stringPiece, substitutions[index] !== undefined ? substitutions[index] : ""]);
const stringLines = interleavedSubstitutions.join("").split("\n");
+88
View File
@@ -0,0 +1,88 @@
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 type SeparatorDirection = "Horizontal" | "Vertical";
export type SeparatorType = "Related" | "Unrelated" | "Section" | "List";
export interface SeparatorWidget {
kind: "Separator";
props: SeparatorProps;
}
export interface SeparatorProps {
direction?: SeparatorDirection;
type?: SeparatorType;
}