mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 04:38:12 +08:00
Vue initialization and FloatingMenu codebase refactoring and cleanup (#649)
* Clean up Vue initialization-related code * Rename folder: dispatcher -> interop * Rename folder: state -> providers * Comments and clarification * Rename JS dispatcher to subscription router * Assorted cleanup and renaming * Rename: js-messages.ts -> messages.ts * Comments * Remove unused Vue component injects * Clean up coming soon and add warning about freezing the app * Further cleanup * Dangerous changes * Simplify App.vue code * Move more disparate init code from components into managers * Rename folder: providers -> state-providers * Other * Move Document panel options bar separator to backend * Add destructors to managers to fix HMR * Comments and code style * Rename variable: font -> font_file_url * Fix async font loading; refactor janky floating menu openness and min-width measurement; fix Vetur errors * Fix misaligned canvas in viewport until panning on page (re)load * Add Vue bidirectional props documentation * More folder renaming for better terminology; add some documentation
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { HSVA, RGBA } from "@/wasm-communication/messages";
|
||||
|
||||
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;
|
||||
|
||||
const p = v * (1 - s);
|
||||
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 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) {
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return { h, s, v, a };
|
||||
}
|
||||
|
||||
export function rgbaToDecimalRgba(rgba: RGBA): RGBA {
|
||||
const r = rgba.r / 255;
|
||||
const g = rgba.g / 255;
|
||||
const b = rgba.b / 255;
|
||||
|
||||
return { r, g, b, a: rgba.a };
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export function downloadBlob(url: string, filename: string): void {
|
||||
const element = document.createElement("a");
|
||||
|
||||
element.href = url;
|
||||
element.setAttribute("download", filename);
|
||||
element.style.display = "none";
|
||||
|
||||
element.click();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
downloadBlob(url, filename);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
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";
|
||||
element.style.display = "none";
|
||||
element.accept = acceptedEextensions;
|
||||
|
||||
element.addEventListener(
|
||||
"change",
|
||||
async () => {
|
||||
if (element.files?.length) {
|
||||
const file = element.files[0];
|
||||
const filename = file.name;
|
||||
const content = await file.text();
|
||||
|
||||
resolve({ filename, content });
|
||||
}
|
||||
},
|
||||
{ capture: false, once: true }
|
||||
);
|
||||
|
||||
element.click();
|
||||
|
||||
// Once `element` goes out of scope, it has no references so it gets garbage collected along with its event listener, so `removeEventListener` is not needed
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/* eslint-disable import/first */
|
||||
|
||||
// 12px Solid
|
||||
import Checkmark from "@/../assets/12px-solid/checkmark.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 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";
|
||||
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";
|
||||
|
||||
const SOLID_12PX = {
|
||||
Checkmark: { component: Checkmark, size: 12 },
|
||||
CloseX: { component: CloseX, size: 12 },
|
||||
DropdownArrow: { component: DropdownArrow, size: 12 },
|
||||
FullscreenEnter: { component: FullscreenEnter, size: 12 },
|
||||
FullscreenExit: { component: FullscreenExit, size: 12 },
|
||||
Grid: { component: Grid, size: 12 },
|
||||
Info: { component: Info, size: 12 },
|
||||
KeyboardArrowDown: { component: KeyboardArrowDown, size: 12 },
|
||||
KeyboardArrowLeft: { component: KeyboardArrowLeft, size: 12 },
|
||||
KeyboardArrowRight: { component: KeyboardArrowRight, size: 12 },
|
||||
KeyboardArrowUp: { component: KeyboardArrowUp, 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 },
|
||||
Link: { component: Link, size: 12 },
|
||||
Overlays: { component: Overlays, size: 12 },
|
||||
ResetColors: { component: ResetColors, size: 12 },
|
||||
Snapping: { component: Snapping, size: 12 },
|
||||
Swap: { component: Swap, size: 12 },
|
||||
VerticalEllipsis: { component: VerticalEllipsis, size: 12 },
|
||||
Warning: { component: Warning, size: 12 },
|
||||
WindowButtonWinClose: { component: WindowButtonWinClose, size: 12 },
|
||||
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: 12 },
|
||||
WindowButtonWinMinimize: { component: WindowButtonWinMinimize, size: 12 },
|
||||
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: 12 },
|
||||
} as const;
|
||||
|
||||
// 16px Solid
|
||||
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 NodeArtboard from "@/../assets/16px-solid/node-artboard.svg";
|
||||
import NodeBlur from "@/../assets/16px-solid/node-blur.svg";
|
||||
import NodeBrushwork from "@/../assets/16px-solid/node-brushwork.svg";
|
||||
import NodeColorCorrection from "@/../assets/16px-solid/node-color-correction.svg";
|
||||
import NodeFolder from "@/../assets/16px-solid/node-folder.svg";
|
||||
import NodeGradient from "@/../assets/16px-solid/node-gradient.svg";
|
||||
import NodeImage from "@/../assets/16px-solid/node-image.svg";
|
||||
import NodeMagicWand from "@/../assets/16px-solid/node-magic-wand.svg";
|
||||
import NodeMask from "@/../assets/16px-solid/node-mask.svg";
|
||||
import NodeMotionBlur from "@/../assets/16px-solid/node-motion-blur.svg";
|
||||
import NodeOutput from "@/../assets/16px-solid/node-output.svg";
|
||||
import NodeShape from "@/../assets/16px-solid/node-shape.svg";
|
||||
import NodeText from "@/../assets/16px-solid/node-text.svg";
|
||||
import NodeTransform from "@/../assets/16px-solid/node-transform.svg";
|
||||
import Paste from "@/../assets/16px-solid/paste.svg";
|
||||
import Trash from "@/../assets/16px-solid/trash.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";
|
||||
|
||||
const SOLID_16PX = {
|
||||
AlignBottom: { component: AlignBottom, size: 16 },
|
||||
AlignHorizontalCenter: { component: AlignHorizontalCenter, size: 16 },
|
||||
AlignLeft: { component: AlignLeft, size: 16 },
|
||||
AlignRight: { component: AlignRight, size: 16 },
|
||||
AlignTop: { component: AlignTop, size: 16 },
|
||||
AlignVerticalCenter: { component: AlignVerticalCenter, size: 16 },
|
||||
BooleanDifference: { component: BooleanDifference, size: 16 },
|
||||
BooleanIntersect: { component: BooleanIntersect, size: 16 },
|
||||
BooleanSubtractBack: { component: BooleanSubtractBack, size: 16 },
|
||||
BooleanSubtractFront: { component: BooleanSubtractFront, size: 16 },
|
||||
BooleanUnion: { component: BooleanUnion, size: 16 },
|
||||
Copy: { component: Copy, size: 16 },
|
||||
EyeHidden: { component: EyeHidden, size: 16 },
|
||||
EyeVisible: { component: EyeVisible, size: 16 },
|
||||
File: { component: File, size: 16 },
|
||||
FlipHorizontal: { component: FlipHorizontal, size: 16 },
|
||||
FlipVertical: { component: FlipVertical, size: 16 },
|
||||
GraphiteLogo: { component: GraphiteLogo, size: 16 },
|
||||
NodeArtboard: { component: NodeArtboard, size: 16 },
|
||||
NodeBlur: { component: NodeBlur, size: 16 },
|
||||
NodeBrushwork: { component: NodeBrushwork, size: 16 },
|
||||
NodeColorCorrection: { component: NodeColorCorrection, size: 16 },
|
||||
NodeFolder: { component: NodeFolder, size: 16 },
|
||||
NodeGradient: { component: NodeGradient, size: 16 },
|
||||
NodeImage: { component: NodeImage, size: 16 },
|
||||
NodeMagicWand: { component: NodeMagicWand, size: 16 },
|
||||
NodeMask: { component: NodeMask, size: 16 },
|
||||
NodeMotionBlur: { component: NodeMotionBlur, size: 16 },
|
||||
NodeOutput: { component: NodeOutput, size: 16 },
|
||||
NodeShape: { component: NodeShape, size: 16 },
|
||||
NodeText: { component: NodeText, size: 16 },
|
||||
NodeTransform: { component: NodeTransform, size: 16 },
|
||||
Paste: { component: Paste, size: 16 },
|
||||
Trash: { component: Trash, size: 16 },
|
||||
ViewModeNormal: { component: ViewModeNormal, size: 16 },
|
||||
ViewModeOutline: { component: ViewModeOutline, size: 16 },
|
||||
ViewModePixels: { component: ViewModePixels, size: 16 },
|
||||
ViewportDesignMode: { component: ViewportDesignMode, size: 16 },
|
||||
ViewportGuideMode: { component: ViewportGuideMode, size: 16 },
|
||||
ViewportSelectMode: { component: ViewportSelectMode, size: 16 },
|
||||
ZoomIn: { component: ZoomIn, size: 16 },
|
||||
ZoomOut: { component: ZoomOut, size: 16 },
|
||||
ZoomReset: { component: ZoomReset, size: 16 },
|
||||
} as const;
|
||||
|
||||
// 16px Two-Tone
|
||||
import MouseHintDrag from "@/../assets/16px-two-tone/mouse-hint-drag.svg";
|
||||
import MouseHintLmbDrag from "@/../assets/16px-two-tone/mouse-hint-lmb-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";
|
||||
|
||||
const TWO_TONE_16PX = {
|
||||
MouseHintDrag: { component: MouseHintDrag, size: 16 },
|
||||
MouseHintLmb: { component: MouseHintLmb, size: 16 },
|
||||
MouseHintLmbDrag: { component: MouseHintLmbDrag, size: 16 },
|
||||
MouseHintMmb: { component: MouseHintMmb, size: 16 },
|
||||
MouseHintMmbDrag: { component: MouseHintMmbDrag, size: 16 },
|
||||
MouseHintNone: { component: MouseHintNone, size: 16 },
|
||||
MouseHintRmb: { component: MouseHintRmb, size: 16 },
|
||||
MouseHintRmbDrag: { component: MouseHintRmbDrag, size: 16 },
|
||||
MouseHintScrollDown: { component: MouseHintScrollDown, size: 16 },
|
||||
MouseHintScrollUp: { component: MouseHintScrollUp, size: 16 },
|
||||
} as const;
|
||||
|
||||
// 24px Two-Tone
|
||||
import GeneralArtboardTool from "@/../assets/24px-two-tone/general-artboard-tool.svg";
|
||||
import GeneralEyedropperTool from "@/../assets/24px-two-tone/general-eyedropper-tool.svg";
|
||||
import GeneralFillTool from "@/../assets/24px-two-tone/general-fill-tool.svg";
|
||||
import GeneralGradientTool from "@/../assets/24px-two-tone/general-gradient-tool.svg";
|
||||
import GeneralNavigateTool from "@/../assets/24px-two-tone/general-navigate-tool.svg";
|
||||
import GeneralSelectTool from "@/../assets/24px-two-tone/general-select-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 RasterDetailTool 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";
|
||||
import VectorTextTool from "@/../assets/24px-two-tone/vector-text-tool.svg";
|
||||
|
||||
const TWO_TONE_24PX = {
|
||||
GeneralArtboardTool: { component: GeneralArtboardTool, size: 24 },
|
||||
GeneralEyedropperTool: { component: GeneralEyedropperTool, size: 24 },
|
||||
GeneralNavigateTool: { component: GeneralNavigateTool, size: 24 },
|
||||
GeneralSelectTool: { component: GeneralSelectTool, size: 24 },
|
||||
GeneralFillTool: { component: GeneralFillTool, size: 24 },
|
||||
GeneralGradientTool: { component: GeneralGradientTool, size: 24 },
|
||||
RasterBrushTool: { component: RasterBrushTool, size: 24 },
|
||||
RasterCloneTool: { component: RasterCloneTool, size: 24 },
|
||||
RasterDetailTool: { component: RasterDetailTool, size: 24 },
|
||||
RasterHealTool: { component: RasterHealTool, size: 24 },
|
||||
RasterPatchTool: { component: RasterPatchTool, size: 24 },
|
||||
RasterRelightTool: { component: RasterRelightTool, size: 24 },
|
||||
VectorEllipseTool: { component: VectorEllipseTool, size: 24 },
|
||||
VectorFreehandTool: { component: VectorFreehandTool, size: 24 },
|
||||
VectorLineTool: { component: VectorLineTool, size: 24 },
|
||||
VectorPathTool: { component: VectorPathTool, size: 24 },
|
||||
VectorPenTool: { component: VectorPenTool, size: 24 },
|
||||
VectorRectangleTool: { component: VectorRectangleTool, size: 24 },
|
||||
VectorShapeTool: { component: VectorShapeTool, size: 24 },
|
||||
VectorSplineTool: { component: VectorSplineTool, size: 24 },
|
||||
VectorTextTool: { component: VectorTextTool, size: 24 },
|
||||
} as const;
|
||||
|
||||
// All icons
|
||||
const ICON_LIST = {
|
||||
...SOLID_12PX,
|
||||
...SOLID_16PX,
|
||||
...TWO_TONE_16PX,
|
||||
...TWO_TONE_24PX,
|
||||
} as const;
|
||||
|
||||
// Exported icons and types
|
||||
export const icons: IconDefinitionType<typeof ICON_LIST> = ICON_LIST;
|
||||
export const iconComponents = Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component]));
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
export type IconSize = 12 | 16 | 24 | 32;
|
||||
export type IconStyle = "node" | "";
|
||||
|
||||
// The following helper type declarations allow us to avoid manually maintaining the `IconName` type declaration as a string union paralleling the keys of the
|
||||
// icon definitions. It lets TypeScript do that for us. Our goal is to define the big key-value pair of icons by constraining its values, but inferring its keys.
|
||||
// Constraining its values means that TypeScript can make sure each icon definition has a valid size number from the union of numbers that is `IconSize`.
|
||||
// Inferring its keys means we don't have to specify a supertype like `string` or `any` for the key-value pair's keys, which would prevent us from accessing
|
||||
// the individual keys with `keyof typeof`. Absent a specified type for the keys, TypeScript falls back to inferring that the key-value pair's type is the
|
||||
// map of all its individual entries. Having the full list of entries lets us automatically set the `IconName` type to the union of strings that is the full
|
||||
// list of keys. The result is that we don't have to maintain a separate list of icon names since this scheme infers it from the keys of the icon definitions.
|
||||
// Based on https://stackoverflow.com/a/64119715/775283
|
||||
type IconDefinition = { component: string; size: IconSize };
|
||||
type EvaluateType<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
||||
type IconDefinitionType<T extends Record<string, IconDefinition>> = EvaluateType<{ [key in keyof T]: IconDefinition }>;
|
||||
@@ -0,0 +1,367 @@
|
||||
export function makeKeyboardModifiersBitfield(e: WheelEvent | PointerEvent | KeyboardEvent): number {
|
||||
return Number(e.ctrlKey) | (Number(e.shiftKey) << 1) | (Number(e.altKey) << 2);
|
||||
}
|
||||
|
||||
// Necessary because innerText puts an extra newline character at the end when the text is more than one line.
|
||||
export function textInputCleanup(text: string): string {
|
||||
if (text[text.length - 1] === "\n") return text.slice(0, -1);
|
||||
return text;
|
||||
}
|
||||
|
||||
// This function is a naive, temporary solution to allow non-Latin keyboards to fall back on the physical QWERTY layout
|
||||
export function getLatinKey(e: KeyboardEvent): string | null {
|
||||
const key = e.key.toLowerCase();
|
||||
const isPrintable = !ALL_PRINTABLE_KEYS.has(e.key);
|
||||
|
||||
// Control (non-printable) characters are handled normally
|
||||
if (!isPrintable) return key;
|
||||
|
||||
// These non-Latin characters should fall back to the Latin equivalent at the key location
|
||||
const LAST_LATIN_UNICODE_CHAR = 0x024f;
|
||||
if (key.length > 1 || key.charCodeAt(0) > LAST_LATIN_UNICODE_CHAR) return keyCodeToKey(e.code);
|
||||
|
||||
// Otherwise, ths is a printable Latin character
|
||||
return e.key.toLowerCase();
|
||||
}
|
||||
|
||||
export function keyCodeToKey(code: string): string | null {
|
||||
// Letters
|
||||
if (code.match(/^Key[A-Z]$/)) return code.replace("Key", "").toLowerCase();
|
||||
|
||||
// Numbers
|
||||
if (code.match(/^Digit[0-9]$/)) return code.replace("Digit", "");
|
||||
if (code.match(/^Numpad[0-9]$/)) return code.replace("Numpad", "");
|
||||
|
||||
// Function keys
|
||||
if (code.match(/^F[1-9]|F1[0-9]|F20$/)) return code.replace("F", "").toLowerCase();
|
||||
|
||||
// Other characters
|
||||
if (SPECIAL_CHARACTERS[code]) return SPECIAL_CHARACTERS[code];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const SPECIAL_CHARACTERS: Record<string, string> = {
|
||||
BracketLeft: "[",
|
||||
BracketRight: "]",
|
||||
Backslash: "\\",
|
||||
Slash: "/",
|
||||
Period: ".",
|
||||
Comma: ",",
|
||||
Equal: "=",
|
||||
Minus: "-",
|
||||
Quote: "'",
|
||||
Semicolon: ";",
|
||||
NumpadEqual: "=",
|
||||
NumpadDivide: "/",
|
||||
NumpadMultiply: "*",
|
||||
NumpadSubtract: "-",
|
||||
NumpadAdd: "+",
|
||||
NumpadDecimal: ".",
|
||||
} as const;
|
||||
|
||||
const ALL_PRINTABLE_KEYS = new Set([
|
||||
// Modifier
|
||||
"Alt",
|
||||
"AltGraph",
|
||||
"CapsLock",
|
||||
"Control",
|
||||
"Fn",
|
||||
"FnLock",
|
||||
"Meta",
|
||||
"NumLock",
|
||||
"ScrollLock",
|
||||
"Shift",
|
||||
"Symbol",
|
||||
"SymbolLock",
|
||||
// Legacy modifier
|
||||
"Hyper",
|
||||
"Super",
|
||||
// White space
|
||||
"Enter",
|
||||
"Tab",
|
||||
// Navigation
|
||||
"ArrowDown",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"End",
|
||||
"Home",
|
||||
"PageDown",
|
||||
"PageUp",
|
||||
// Editing
|
||||
"Backspace",
|
||||
"Clear",
|
||||
"Copy",
|
||||
"CrSel",
|
||||
"Cut",
|
||||
"Delete",
|
||||
"EraseEof",
|
||||
"ExSel",
|
||||
"Insert",
|
||||
"Paste",
|
||||
"Redo",
|
||||
"Undo",
|
||||
// UI
|
||||
"Accept",
|
||||
"Again",
|
||||
"Attn",
|
||||
"Cancel",
|
||||
"ContextMenu",
|
||||
"Escape",
|
||||
"Execute",
|
||||
"Find",
|
||||
"Help",
|
||||
"Pause",
|
||||
"Play",
|
||||
"Props",
|
||||
"Select",
|
||||
"ZoomIn",
|
||||
"ZoomOut",
|
||||
// Device
|
||||
"BrightnessDown",
|
||||
"BrightnessUp",
|
||||
"Eject",
|
||||
"LogOff",
|
||||
"Power",
|
||||
"PowerOff",
|
||||
"PrintScreen",
|
||||
"Hibernate",
|
||||
"Standby",
|
||||
"WakeUp",
|
||||
// IME composition keys
|
||||
"AllCandidates",
|
||||
"Alphanumeric",
|
||||
"CodeInput",
|
||||
"Compose",
|
||||
"Convert",
|
||||
"Dead",
|
||||
"FinalMode",
|
||||
"GroupFirst",
|
||||
"GroupLast",
|
||||
"GroupNext",
|
||||
"GroupPrevious",
|
||||
"ModeChange",
|
||||
"NextCandidate",
|
||||
"NonConvert",
|
||||
"PreviousCandidate",
|
||||
"Process",
|
||||
"SingleCandidate",
|
||||
// Korean-specific
|
||||
"HangulMode",
|
||||
"HanjaMode",
|
||||
"JunjaMode",
|
||||
// Japanese-specific
|
||||
"Eisu",
|
||||
"Hankaku",
|
||||
"Hiragana",
|
||||
"HiraganaKatakana",
|
||||
"KanaMode",
|
||||
"KanjiMode",
|
||||
"Katakana",
|
||||
"Romaji",
|
||||
"Zenkaku",
|
||||
"ZenkakuHankaku",
|
||||
// Common function
|
||||
"F1",
|
||||
"F2",
|
||||
"F3",
|
||||
"F4",
|
||||
"F5",
|
||||
"F6",
|
||||
"F7",
|
||||
"F8",
|
||||
"F9",
|
||||
"F10",
|
||||
"F11",
|
||||
"F12",
|
||||
"Soft1",
|
||||
"Soft2",
|
||||
"Soft3",
|
||||
"Soft4",
|
||||
// Multimedia
|
||||
"ChannelDown",
|
||||
"ChannelUp",
|
||||
"Close",
|
||||
"MailForward",
|
||||
"MailReply",
|
||||
"MailSend",
|
||||
"MediaClose",
|
||||
"MediaFastForward",
|
||||
"MediaPause",
|
||||
"MediaPlay",
|
||||
"MediaPlayPause",
|
||||
"MediaRecord",
|
||||
"MediaRewind",
|
||||
"MediaStop",
|
||||
"MediaTrackNext",
|
||||
"MediaTrackPrevious",
|
||||
"New",
|
||||
"Open",
|
||||
"Print",
|
||||
"Save",
|
||||
"SpellCheck",
|
||||
// Multimedia numpad
|
||||
"Key11",
|
||||
"Key12",
|
||||
// Audio
|
||||
"AudioBalanceLeft",
|
||||
"AudioBalanceRight",
|
||||
"AudioBassBoostDown",
|
||||
"AudioBassBoostToggle",
|
||||
"AudioBassBoostUp",
|
||||
"AudioFaderFront",
|
||||
"AudioFaderRear",
|
||||
"AudioSurroundModeNext",
|
||||
"AudioTrebleDown",
|
||||
"AudioTrebleUp",
|
||||
"AudioVolumeDown",
|
||||
"AudioVolumeUp",
|
||||
"AudioVolumeMute",
|
||||
"MicrophoneToggle",
|
||||
"MicrophoneVolumeDown",
|
||||
"MicrophoneVolumeUp",
|
||||
"MicrophoneVolumeMute",
|
||||
// Speech
|
||||
"SpeechCorrectionList",
|
||||
"SpeechInputToggle",
|
||||
// Application
|
||||
"LaunchApplication1",
|
||||
"LaunchApplication2",
|
||||
"LaunchCalendar",
|
||||
"LaunchContacts",
|
||||
"LaunchMail",
|
||||
"LaunchMediaPlayer",
|
||||
"LaunchMusicPlayer",
|
||||
"LaunchPhone",
|
||||
"LaunchScreenSaver",
|
||||
"LaunchSpreadsheet",
|
||||
"LaunchWebBrowser",
|
||||
"LaunchWebCam",
|
||||
"LaunchWordProcessor",
|
||||
// Browser
|
||||
"BrowserBack",
|
||||
"BrowserFavorites",
|
||||
"BrowserForward",
|
||||
"BrowserHome",
|
||||
"BrowserRefresh",
|
||||
"BrowserSearch",
|
||||
"BrowserStop",
|
||||
// Mobile phone
|
||||
"AppSwitch",
|
||||
"Call",
|
||||
"Camera",
|
||||
"CameraFocus",
|
||||
"EndCall",
|
||||
"GoBack",
|
||||
"GoHome",
|
||||
"HeadsetHook",
|
||||
"LastNumberRedial",
|
||||
"Notification",
|
||||
"MannerMode",
|
||||
"VoiceDial",
|
||||
// TV
|
||||
"TV",
|
||||
"TV3DMode",
|
||||
"TVAntennaCable",
|
||||
"TVAudioDescription",
|
||||
"TVAudioDescriptionMixDown",
|
||||
"TVAudioDescriptionMixUp",
|
||||
"TVContentsMenu",
|
||||
"TVDataService",
|
||||
"TVInput",
|
||||
"TVInputComponent1",
|
||||
"TVInputComponent2",
|
||||
"TVInputComposite1",
|
||||
"TVInputComposite2",
|
||||
"TVInputHDMI1",
|
||||
"TVInputHDMI2",
|
||||
"TVInputHDMI3",
|
||||
"TVInputHDMI4",
|
||||
"TVInputVGA1",
|
||||
"TVMediaContext",
|
||||
"TVNetwork",
|
||||
"TVNumberEntry",
|
||||
"TVPower",
|
||||
"TVRadioService",
|
||||
"TVSatellite",
|
||||
"TVSatelliteBS",
|
||||
"TVSatelliteCS",
|
||||
"TVSatelliteToggle",
|
||||
"TVTerrestrialAnalog",
|
||||
"TVTerrestrialDigital",
|
||||
"TVTimer",
|
||||
// Media controls
|
||||
"AVRInput",
|
||||
"AVRPower",
|
||||
"ColorF0Red",
|
||||
"ColorF1Green",
|
||||
"ColorF2Yellow",
|
||||
"ColorF3Blue",
|
||||
"ColorF4Grey",
|
||||
"ColorF5Brown",
|
||||
"ClosedCaptionToggle",
|
||||
"Dimmer",
|
||||
"DisplaySwap",
|
||||
"DVR",
|
||||
"Exit",
|
||||
"FavoriteClear0",
|
||||
"FavoriteClear1",
|
||||
"FavoriteClear2",
|
||||
"FavoriteClear3",
|
||||
"FavoriteRecall0",
|
||||
"FavoriteRecall1",
|
||||
"FavoriteRecall2",
|
||||
"FavoriteRecall3",
|
||||
"FavoriteStore0",
|
||||
"FavoriteStore1",
|
||||
"FavoriteStore2",
|
||||
"FavoriteStore3",
|
||||
"Guide",
|
||||
"GuideNextDay",
|
||||
"GuidePreviousDay",
|
||||
"Info",
|
||||
"InstantReplay",
|
||||
"Link",
|
||||
"ListProgram",
|
||||
"LiveContent",
|
||||
"Lock",
|
||||
"MediaApps",
|
||||
"MediaAudioTrack",
|
||||
"MediaLast",
|
||||
"MediaSkipBackward",
|
||||
"MediaSkipForward",
|
||||
"MediaStepBackward",
|
||||
"MediaStepForward",
|
||||
"MediaTopMenu",
|
||||
"NavigateIn",
|
||||
"NavigateNext",
|
||||
"NavigateOut",
|
||||
"NavigatePrevious",
|
||||
"NextFavoriteChannel",
|
||||
"NextUserProfile",
|
||||
"OnDemand",
|
||||
"Pairing",
|
||||
"PinPDown",
|
||||
"PinPMove",
|
||||
"PinPToggle",
|
||||
"PinPUp",
|
||||
"PlaySpeedDown",
|
||||
"PlaySpeedReset",
|
||||
"PlaySpeedUp",
|
||||
"RandomToggle",
|
||||
"RcLowBattery",
|
||||
"RecordSpeedNext",
|
||||
"RfBypass",
|
||||
"ScanChannelsToggle",
|
||||
"ScreenModeNext",
|
||||
"Settings",
|
||||
"SplitScreenToggle",
|
||||
"STBInput",
|
||||
"STBPower",
|
||||
"Subtitle",
|
||||
"Teletext",
|
||||
"VideoModeNext",
|
||||
"Wink",
|
||||
"ZoomToggle",
|
||||
]);
|
||||
@@ -0,0 +1,3 @@
|
||||
export function clamp(value: number, min = 0, max = 1): number {
|
||||
return Math.max(min, Math.min(value, max));
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// This works by proxying every function call and wrapping a try-catch block to filter out redundant and confusing
|
||||
// `RuntimeError: unreachable` exceptions that would normally be printed in the browser's JS console upon a panic.
|
||||
export function panicProxy<T extends object>(module: T): T {
|
||||
const proxyHandler = {
|
||||
get(target: T, propKey: string | symbol, receiver: unknown): unknown {
|
||||
const targetValue = Reflect.get(target, propKey, receiver);
|
||||
|
||||
// Keep the original value being accessed if it isn't a function
|
||||
const isFunction = typeof targetValue === "function";
|
||||
if (!isFunction) return targetValue;
|
||||
|
||||
// Special handling to wrap the return of a constructor in the proxy
|
||||
const isClass = isFunction && /^\s*class\s+/.test(targetValue.toString());
|
||||
if (isClass) {
|
||||
// eslint-disable-next-line func-names
|
||||
return function (...args: unknown[]): unknown {
|
||||
// eslint-disable-next-line new-cap
|
||||
const result = new targetValue(...args);
|
||||
return panicProxy(result);
|
||||
};
|
||||
}
|
||||
|
||||
// Replace the original function with a wrapper function that runs the original in a try-catch block
|
||||
// eslint-disable-next-line func-names
|
||||
return function (...args: unknown[]): unknown {
|
||||
let result;
|
||||
try {
|
||||
// @ts-expect-error TypeScript does not know what `this` is, since it should be able to be anything
|
||||
result = targetValue.apply(this, args);
|
||||
} catch (err) {
|
||||
// Suppress `unreachable` WebAssembly.RuntimeError exceptions
|
||||
if (!`${err}`.startsWith("RuntimeError: unreachable")) throw err;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return new Proxy<T>(module, proxyHandler);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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");
|
||||
|
||||
const visibleLineTabPrefixLengths = stringLines.map((line) => (line.match(/\S/) ? (line.match(/^(\t*)/) || [])[1].length : Infinity));
|
||||
const commonTabPrefixLength = Math.min(...visibleLineTabPrefixLengths);
|
||||
|
||||
const linesWithoutCommonTabPrefix = stringLines.map((line) => line.substring(commonTabPrefixLength));
|
||||
const multiLineString = linesWithoutCommonTabPrefix.join("\n");
|
||||
|
||||
return multiLineString.trim();
|
||||
}
|
||||
Reference in New Issue
Block a user