Replace the Vue frontend with Svelte

This commit is contained in:
Keavon Chambers
2023-03-10 03:54:39 -08:00
parent e539e43483
commit 6e20ea538b
83 changed files with 10013 additions and 19687 deletions
@@ -1,56 +1,49 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type IconName, ICONS, ICON_SVG_STRINGS } from "@/utility-functions/icons";
import { type IconName, ICONS, ICON_COMPONENTS } from "@/utility-functions/icons";
import LayoutRow from "@/components/layout/LayoutRow.svelte";
import LayoutRow from "@/components/layout/LayoutRow.vue";
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
export let icon: IconName;
export let disabled = false;
export let tooltip: string | undefined = undefined;
export default defineComponent({
props: {
icon: { type: String as PropType<IconName>, required: true },
disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
},
computed: {
iconSizeClass(): string {
return `size-${ICONS[this.icon].size}`;
},
},
components: {
LayoutRow,
...ICON_COMPONENTS,
},
});
$: iconSizeClass = ((icon: IconName) => {
return `size-${ICONS[icon].size}`;
})(icon);
$: extraClasses = Object.entries(classes)
.flatMap((classAndState) => (classAndState[1] ? [classAndState[0]] : []))
.join(" ");
</script>
<template>
<LayoutRow :class="['icon-label', iconSizeClass, { disabled }]" :title="tooltip">
<component :is="icon" />
</LayoutRow>
</template>
<LayoutRow class={`icon-label ${iconSizeClass} ${className} ${extraClasses}`.trim()} classes={{ disabled }} {tooltip}>
{@html ICON_SVG_STRINGS[icon]}
</LayoutRow>
<style lang="scss">
.icon-label {
flex: 0 0 auto;
fill: var(--color-e-nearwhite);
<style lang="scss" global>
.icon-label {
flex: 0 0 auto;
fill: var(--color-e-nearwhite);
&.disabled {
fill: var(--color-8-uppergray);
&.disabled {
fill: var(--color-8-uppergray);
}
&.size-12 {
width: 12px;
height: 12px;
}
&.size-16 {
width: 16px;
height: 16px;
}
&.size-24 {
width: 24px;
height: 24px;
}
}
&.size-12 {
width: 12px;
height: 12px;
}
&.size-16 {
width: 16px;
height: 16px;
}
&.size-24 {
width: 24px;
height: 24px;
}
}
</style>
@@ -1,86 +1,80 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type SeparatorDirection, type SeparatorType } from "@/wasm-communication/messages";
import { type SeparatorDirection, type SeparatorType } from "@/wasm-communication/messages";
export default defineComponent({
props: {
direction: { type: String as PropType<SeparatorDirection>, default: "Horizontal" },
type: { type: String as PropType<SeparatorType>, default: "Unrelated" },
},
});
export let direction: SeparatorDirection = "Horizontal";
export let type: SeparatorType = "Unrelated";
</script>
<template>
<div class="separator" :class="[direction.toLowerCase(), type.toLowerCase()]">
<div v-if="['Section', 'List'].includes(type)"></div>
</div>
</template>
<div class={`separator ${direction.toLowerCase()} ${type.toLowerCase()}`}>
{#if ["Section", "List"].includes(type)}
<div />
{/if}
</div>
<style lang="scss">
.separator {
&.vertical {
flex: 0 0 auto;
<style lang="scss" global>
.separator {
&.vertical {
flex: 0 0 auto;
&.related {
height: 4px;
}
&.unrelated {
height: 8px;
}
&.section,
&.list {
width: 100%;
div {
height: 1px;
width: calc(100% - 8px);
margin: 0 4px;
background: var(--color-7-middlegray);
&.related {
height: 4px;
}
}
&.section {
margin: 8px 0;
}
&.unrelated {
height: 8px;
}
&.list {
margin: 4px 0;
}
}
&.section,
&.list {
width: 100%;
&.horizontal {
flex: 0 0 auto;
div {
height: 1px;
width: calc(100% - 8px);
margin: 0 4px;
background: var(--color-7-middlegray);
}
}
&.related {
width: 4px;
}
&.section {
margin: 8px 0;
}
&.unrelated {
width: 8px;
}
&.section,
&.list {
height: 100%;
div {
height: calc(100% - 8px);
width: 1px;
&.list {
margin: 4px 0;
background: var(--color-7-middlegray);
}
}
&.section {
margin: 0 8px;
}
&.horizontal {
flex: 0 0 auto;
&.list {
margin: 0 4px;
&.related {
width: 4px;
}
&.unrelated {
width: 8px;
}
&.section,
&.list {
height: 100%;
div {
height: calc(100% - 8px);
width: 1px;
margin: 4px 0;
background: var(--color-7-middlegray);
}
}
&.section {
margin: 0 8px;
}
&.list {
margin: 0 4px;
}
}
}
}
</style>
@@ -1,52 +1,67 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
let styleName = "";
export { styleName as style };
export let styles: Record<string, string | number | undefined> = {};
export let disabled = false;
export let bold = false;
export let italic = false;
export let tableAlign = false;
export let minWidth = 0;
export let multiline = false;
export let tooltip: string | undefined = undefined;
export default defineComponent({
props: {
disabled: { type: Boolean as PropType<boolean>, default: false },
bold: { type: Boolean as PropType<boolean>, default: false },
italic: { type: Boolean as PropType<boolean>, default: false },
tableAlign: { type: Boolean as PropType<boolean>, default: false },
minWidth: { type: Number as PropType<number>, default: 0 },
multiline: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
},
});
$: extraClasses = Object.entries(classes)
.flatMap((classAndState) => (classAndState[1] ? [classAndState[0]] : []))
.join(" ");
$: extraStyles = Object.entries(styles)
.flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : []))
.join(" ");
</script>
<template>
<span class="text-label" :class="{ disabled, bold, italic, multiline, 'table-align': tableAlign }" :style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }" :title="tooltip">
<slot></slot>
</span>
</template>
<span
class={`text-label ${className} ${extraClasses}`.trim()}
class:disabled
class:bold
class:italic
class:multiline
class:table-align={tableAlign}
style:min-width={minWidth > 0 ? `${minWidth}px` : undefined}
style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip}
>
<slot />
</span>
<style lang="scss">
.text-label {
line-height: 18px;
white-space: nowrap;
// Force Safari to not draw a text cursor, even though this element has `user-select: none`
cursor: default;
<style lang="scss" global>
.text-label {
line-height: 18px;
white-space: nowrap;
// Force Safari to not draw a text cursor, even though this element has `user-select: none`
cursor: default;
&.disabled {
color: var(--color-8-uppergray);
&.disabled {
color: var(--color-8-uppergray);
}
&.bold {
font-weight: 700;
}
&.italic {
font-style: italic;
}
&.multiline {
white-space: pre-wrap;
margin: 4px 0;
}
&.table-align {
flex: 0 0 30%;
text-align: right;
}
}
&.bold {
font-weight: 700;
}
&.italic {
font-style: italic;
}
&.multiline {
white-space: pre-wrap;
margin: 4px 0;
}
&.table-align {
flex: 0 0 30%;
text-align: right;
}
}
</style>
@@ -1,249 +1,248 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type IconName } from "@/utility-functions/icons";
import { platformIsMac } from "@/utility-functions/platform";
import { type KeyRaw, type LayoutKeysGroup, type Key, type MouseMotion } from "@/wasm-communication/messages";
import { type IconName } from "@/utility-functions/icons";
import { platformIsMac } from "@/utility-functions/platform";
import { type KeyRaw, type LayoutKeysGroup, type Key, type MouseMotion } from "@/wasm-communication/messages";
import LayoutRow from "@/components/layout/LayoutRow.svelte";
import IconLabel from "@/components/widgets/labels/IconLabel.svelte";
import Separator from "@/components/widgets/labels/Separator.svelte";
import TextLabel from "@/components/widgets/labels/TextLabel.svelte";
import { getContext } from "svelte";
import { type FullscreenState } from "@/state-providers/fullscreen";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import Separator from "@/components/widgets/labels/Separator.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
type LabelData = { label?: string; icon?: IconName; width: string };
type LabelData = { label?: string; icon?: IconName; width: string };
// Keys that become icons if they are listed here with their units of width
const ICON_WIDTHS_MAC = {
Shift: 2,
Control: 2,
Option: 2,
Command: 2,
};
const ICON_WIDTHS = {
ArrowUp: 1,
ArrowRight: 1,
ArrowDown: 1,
ArrowLeft: 1,
Backspace: 2,
Enter: 2,
Tab: 2,
Space: 3,
...(platformIsMac() ? ICON_WIDTHS_MAC : {}),
};
// Keys that become icons if they are listed here with their units of width
const ICON_WIDTHS_MAC = {
Shift: 2,
Control: 2,
Option: 2,
Command: 2,
};
const ICON_WIDTHS = {
ArrowUp: 1,
ArrowRight: 1,
ArrowDown: 1,
ArrowLeft: 1,
Backspace: 2,
Enter: 2,
Tab: 2,
Space: 3,
...(platformIsMac() ? ICON_WIDTHS_MAC : {}),
};
const fullscreen = getContext<FullscreenState>("fullscreen");
export default defineComponent({
inject: ["fullscreen"],
props: {
keysWithLabelsGroups: { type: Array as PropType<LayoutKeysGroup[]>, default: () => [] },
mouseMotion: { type: String as PropType<MouseMotion | undefined>, required: false },
requiresLock: { type: Boolean as PropType<boolean>, default: false },
},
computed: {
hasSlotContent(): boolean {
return Boolean(this.$slots.default);
},
keyboardLockInfoMessage(): string {
const RESERVED = "This hotkey is reserved by the browser. ";
const USE_FULLSCREEN = "It is made available in fullscreen mode.";
const USE_SECURE_CTX = "It is made available in fullscreen mode when this website is served from a secure context (https or localhost).";
const SWITCH_BROWSER = "Use a Chromium-based browser (like Chrome or Edge) in fullscreen mode to directly use the shortcut.";
export let keysWithLabelsGroups: LayoutKeysGroup[] = [];
export let mouseMotion: MouseMotion | undefined = undefined;
export let requiresLock = false;
if (this.fullscreen.keyboardLockApiSupported) return `${RESERVED} ${USE_FULLSCREEN}`;
if (!("chrome" in window)) return `${RESERVED} ${SWITCH_BROWSER}`;
if (!window.isSecureContext) return `${RESERVED} ${USE_SECURE_CTX}`;
return RESERVED;
},
displayKeyboardLockNotice(): boolean {
return this.requiresLock && !this.fullscreen.state.keyboardLocked;
},
},
methods: {
keyTextOrIconList(keyGroup: LayoutKeysGroup): LabelData[] {
return keyGroup.map((key) => this.keyTextOrIcon(key));
},
keyTextOrIcon(keyWithLabel: Key): LabelData {
// `key` is the name of the `Key` enum in Rust, while `label` is the localized string to display (if it doesn't become an icon)
let key = keyWithLabel.key;
const label = keyWithLabel.label;
$: keyboardLockInfoMessage = watchKeyboardLockInfoMessage(fullscreen.keyboardLockApiSupported);
$: displayKeyboardLockNotice = requiresLock && !$fullscreen.keyboardLocked;
// Replace Alt and Accel keys with their Mac-specific equivalents
if (platformIsMac()) {
if (key === "Alt") key = "Option";
if (key === "Accel") key = "Command";
}
function watchKeyboardLockInfoMessage(keyboardLockApiSupported: boolean): string {
const RESERVED = "This hotkey is reserved by the browser. ";
const USE_FULLSCREEN = "It is made available in fullscreen mode.";
const USE_SECURE_CTX = "It is made available in fullscreen mode when this website is served from a secure context (https or localhost).";
const SWITCH_BROWSER = "Use a Chromium-based browser (like Chrome or Edge) in fullscreen mode to directly use the shortcut.";
// Either display an icon...
// @ts-expect-error We want undefined if it isn't in the object
const iconWidth: number | undefined = ICON_WIDTHS[key];
const icon = iconWidth !== undefined && iconWidth > 0 && (this.keyboardHintIcon(key) || false);
if (icon) return { icon, width: `width-${iconWidth}` };
if (keyboardLockApiSupported) return `${RESERVED} ${USE_FULLSCREEN}`;
if (!("chrome" in window)) return `${RESERVED} ${SWITCH_BROWSER}`;
if (!window.isSecureContext) return `${RESERVED} ${USE_SECURE_CTX}`;
return RESERVED;
}
// ...or display text
return { label, width: `width-${label.length}` };
},
mouseHintIcon(input?: MouseMotion): IconName {
return `MouseHint${input}` as IconName;
},
keyboardHintIcon(input: KeyRaw): IconName | undefined {
switch (input) {
case "ArrowDown":
return "KeyboardArrowDown";
case "ArrowLeft":
return "KeyboardArrowLeft";
case "ArrowRight":
return "KeyboardArrowRight";
case "ArrowUp":
return "KeyboardArrowUp";
case "Backspace":
return "KeyboardBackspace";
case "Command":
return "KeyboardCommand";
case "Control":
return "KeyboardControl";
case "Enter":
return "KeyboardEnter";
case "Option":
return "KeyboardOption";
case "Shift":
return "KeyboardShift";
case "Space":
return "KeyboardSpace";
case "Tab":
return "KeyboardTab";
default:
return undefined;
}
},
},
components: {
IconLabel,
LayoutRow,
Separator,
TextLabel,
},
});
function keyTextOrIconList(keyGroup: LayoutKeysGroup): LabelData[] {
return keyGroup.map((key) => keyTextOrIcon(key));
}
function keyTextOrIcon(keyWithLabel: Key): LabelData {
// `key` is the name of the `Key` enum in Rust, while `label` is the localized string to display (if it doesn't become an icon)
let key = keyWithLabel.key;
const label = keyWithLabel.label;
// Replace Alt and Accel keys with their Mac-specific equivalents
if (platformIsMac()) {
if (key === "Alt") key = "Option";
if (key === "Accel") key = "Command";
}
// Either display an icon...
// @ts-expect-error We want undefined if it isn't in the object
const iconWidth: number | undefined = ICON_WIDTHS[key];
const icon = iconWidth !== undefined && iconWidth > 0 && (keyboardHintIcon(key) || false);
if (icon) return { icon, width: `width-${iconWidth}` };
// ...or display text
return { label, width: `width-${label.length}` };
}
function mouseHintIcon(input?: MouseMotion): IconName {
return `MouseHint${input}` as IconName;
}
function keyboardHintIcon(input: KeyRaw): IconName | undefined {
switch (input) {
case "ArrowDown":
return "KeyboardArrowDown";
case "ArrowLeft":
return "KeyboardArrowLeft";
case "ArrowRight":
return "KeyboardArrowRight";
case "ArrowUp":
return "KeyboardArrowUp";
case "Backspace":
return "KeyboardBackspace";
case "Command":
return "KeyboardCommand";
case "Control":
return "KeyboardControl";
case "Enter":
return "KeyboardEnter";
case "Option":
return "KeyboardOption";
case "Shift":
return "KeyboardShift";
case "Space":
return "KeyboardSpace";
case "Tab":
return "KeyboardTab";
default:
return undefined;
}
}
</script>
<template>
<IconLabel class="user-input-label keyboard-lock-notice" v-if="displayKeyboardLockNotice" :icon="'Info'" :title="keyboardLockInfoMessage" />
<LayoutRow class="user-input-label" v-else>
<template v-for="(keysWithLabels, i) in keysWithLabelsGroups" :key="i">
<Separator :type="'Related'" v-if="i > 0"></Separator>
<template v-for="(keyInfo, j) in keyTextOrIconList(keysWithLabels)" :key="j">
<div class="input-key" :class="keyInfo.width">
<IconLabel v-if="keyInfo.icon" :icon="keyInfo.icon" />
<TextLabel v-else-if="keyInfo.label !== undefined">{{ keyInfo.label }}</TextLabel>
{#if displayKeyboardLockNotice}
<IconLabel class="user-input-label keyboard-lock-notice" icon="Info" tooltip={keyboardLockInfoMessage} />
{:else}
<LayoutRow class="user-input-label">
{#each keysWithLabelsGroups as keysWithLabels, groupIndex (groupIndex)}
{#if groupIndex > 0}
<Separator type="Related" />
{/if}
{#each keyTextOrIconList(keysWithLabels) as keyInfo, keyIndex (keyIndex)}
<div class={`input-key ${keyInfo.width}`}>
{#if keyInfo.icon}
<IconLabel icon={keyInfo.icon} />
{:else if keyInfo.label !== undefined}
<TextLabel>{keyInfo.label}</TextLabel>
{/if}
</div>
</template>
</template>
<div class="input-mouse" v-if="mouseMotion">
<IconLabel :icon="mouseHintIcon(mouseMotion)" />
</div>
<div class="hint-text" v-if="hasSlotContent">
<slot></slot>
</div>
{/each}
{/each}
{#if mouseMotion}
<div class="input-mouse">
<IconLabel icon={mouseHintIcon(mouseMotion)} />
</div>
{/if}
{#if $$slots.default}
<div class="hint-text">
<slot />
</div>
{/if}
</LayoutRow>
</template>
{/if}
<style lang="scss">
.user-input-label {
flex: 0 0 auto;
height: 100%;
align-items: center;
white-space: nowrap;
.input-key,
.input-mouse {
& + .input-key,
& + .input-mouse {
margin-left: 2px;
}
}
.input-key {
display: flex;
justify-content: center;
<style lang="scss" global>
.user-input-label {
flex: 0 0 auto;
height: 100%;
align-items: center;
font-family: "Inconsolata", monospace;
font-weight: 400;
text-align: center;
height: 16px;
box-sizing: border-box;
border: 1px solid;
border-radius: 4px;
border-color: var(--color-5-dullgray);
color: var(--color-e-nearwhite);
white-space: nowrap;
.text-label {
// Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height,
// so moving it up 1 pixel by using 15px makes them agree.
line-height: 15px;
.input-key,
.input-mouse {
& + .input-key,
& + .input-mouse {
margin-left: 2px;
}
}
&.width-1 {
width: 16px;
}
&.width-2 {
width: 24px;
}
&.width-3 {
width: 32px;
}
&.width-4 {
width: 40px;
}
&.width-5 {
width: 48px;
}
.icon-label {
margin: 1px;
}
}
.input-mouse {
.bright {
fill: var(--color-e-nearwhite);
}
.dim {
fill: var(--color-8-uppergray);
}
}
.hint-text {
margin-left: 4px;
}
.floating-menu-content .row > & {
.input-key {
border-color: var(--color-3-darkgray);
color: var(--color-8-uppergray);
display: flex;
justify-content: center;
align-items: center;
font-family: "Inconsolata", monospace;
font-weight: 400;
text-align: center;
height: 16px;
box-sizing: border-box;
border: 1px solid;
border-radius: 4px;
border-color: var(--color-5-dullgray);
color: var(--color-e-nearwhite);
.text-label {
// Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height,
// so moving it up 1 pixel by using 15px makes them agree.
line-height: 15px;
}
&.width-1 {
width: 16px;
}
&.width-2 {
width: 24px;
}
&.width-3 {
width: 32px;
}
&.width-4 {
width: 40px;
}
&.width-5 {
width: 48px;
}
.icon-label {
margin: 1px;
}
}
.input-key .icon-label svg,
&.keyboard-lock-notice.keyboard-lock-notice svg,
.input-mouse .bright {
fill: var(--color-8-uppergray);
.input-mouse {
.bright {
fill: var(--color-e-nearwhite);
}
.dim {
fill: var(--color-8-uppergray);
}
}
.input-mouse .dim {
fill: var(--color-3-darkgray);
.hint-text {
margin-left: 4px;
}
.floating-menu-content .row > & {
.input-key {
border-color: var(--color-3-darkgray);
color: var(--color-8-uppergray);
}
.input-key .icon-label svg,
&.keyboard-lock-notice.keyboard-lock-notice svg,
.input-mouse .bright {
fill: var(--color-8-uppergray);
}
.input-mouse .dim {
fill: var(--color-3-darkgray);
}
}
.floating-menu-content .row:hover > & {
.input-key {
border-color: var(--color-7-middlegray);
}
.input-mouse .dim {
fill: var(--color-7-middlegray);
}
}
}
.floating-menu-content .row:hover > & {
.input-key {
border-color: var(--color-7-middlegray);
}
.input-mouse .dim {
fill: var(--color-7-middlegray);
}
}
}
</style>