mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-20 08:52:59 +00:00
feat: new blocking progress bar, fixes #18
feat: change cloud icon to history, fixes #15 fix: action search items overlap, fixes #16 feat: show tooltips immediately
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
import Navigation from "./Navigation.svelte"
|
||||
import {canAutoConnect} from "$lib/serial/device"
|
||||
import {initSerial} from "$lib/serial/connection"
|
||||
import type {LayoutServerData} from "./$types"
|
||||
import type {LayoutData} from "./$types"
|
||||
import {browser} from "$app/environment"
|
||||
import BrowserWarning from "./BrowserWarning.svelte"
|
||||
import "tippy.js/animations/shift-away.css"
|
||||
@@ -21,12 +21,16 @@
|
||||
import {detectLocale} from "../i18n/i18n-util"
|
||||
import type {Locales} from "../i18n/i18n-types"
|
||||
import Footer from "./Footer.svelte"
|
||||
import {runLayoutDetection} from "$lib/os-layout.js"
|
||||
import PageTransition from "./PageTransition.svelte"
|
||||
import SyncOverlay from "./SyncOverlay.svelte"
|
||||
|
||||
const locale = ((browser && localStorage.getItem("locale")) as Locales) || detectLocale()
|
||||
loadLocale(locale)
|
||||
setLocale(locale)
|
||||
|
||||
if (browser) {
|
||||
runLayoutDetection()
|
||||
tippy.setDefaultProps({
|
||||
animation: "shift-away",
|
||||
theme: "surface-variant",
|
||||
@@ -37,7 +41,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
export let data: LayoutServerData
|
||||
export let data: LayoutData
|
||||
|
||||
onMount(async () => {
|
||||
theme.subscribe(it => {
|
||||
@@ -63,11 +67,15 @@
|
||||
<meta name="theme-color" content={data.themeColor} />
|
||||
</svelte:head>
|
||||
|
||||
<SyncOverlay />
|
||||
|
||||
<Navigation />
|
||||
|
||||
<main>
|
||||
<!-- <PickChangesDialog /> -->
|
||||
|
||||
<PageTransition>
|
||||
<slot />
|
||||
</main>
|
||||
</PageTransition>
|
||||
|
||||
<Footer />
|
||||
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
import type {Change} from "$lib/undo-redo"
|
||||
import {fly} from "svelte/transition"
|
||||
import {action} from "$lib/title"
|
||||
import {deviceChords, deviceLayout, deviceSettings, serialPort, syncStatus} from "$lib/serial/connection"
|
||||
import {askForConfirmation} from "$lib/confirm-dialog"
|
||||
import {
|
||||
deviceChords,
|
||||
deviceLayout,
|
||||
deviceSettings,
|
||||
serialPort,
|
||||
syncProgress,
|
||||
syncStatus,
|
||||
} from "$lib/serial/connection"
|
||||
import {askForConfirmation} from "$lib/dialogs/confirm-dialog"
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
|
||||
function undo(event: MouseEvent) {
|
||||
@@ -94,7 +101,23 @@
|
||||
// would be if they click it every time they change a setting.
|
||||
// Because of that, we don't need to show a fearmongering message such as
|
||||
// "Your device will break after you click this 10,000 times!"
|
||||
await new Promise(resolve => setTimeout(resolve, 6000))
|
||||
const virtualWriteTime = 6000
|
||||
const startStamp = performance.now()
|
||||
await new Promise<void>(resolve => {
|
||||
function animate() {
|
||||
const delta = performance.now() - startStamp
|
||||
syncProgress.set({
|
||||
max: virtualWriteTime,
|
||||
current: delta,
|
||||
})
|
||||
if (delta >= virtualWriteTime) {
|
||||
resolve()
|
||||
} else {
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(animate)
|
||||
})
|
||||
if ($serialPort) {
|
||||
await $serialPort.commit()
|
||||
$changes = []
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
<script>
|
||||
import {version} from "$app/environment"
|
||||
<script lang="ts">
|
||||
import {browser, version} from "$app/environment"
|
||||
import {action} from "$lib/title"
|
||||
import LL, {setLocale} from "../i18n/i18n-svelte"
|
||||
import {theme} from "$lib/preferences.js"
|
||||
import type {Locales} from "../i18n/i18n-types"
|
||||
import {detectLocale, locales} from "../i18n/i18n-util"
|
||||
import {loadLocaleAsync} from "../i18n/i18n-util.async"
|
||||
import {tick} from "svelte"
|
||||
|
||||
let locale = (browser && (localStorage.getItem("locale") as Locales)) || detectLocale()
|
||||
$: if (browser)
|
||||
(async () => {
|
||||
localStorage.setItem("locale", locale)
|
||||
await loadLocaleAsync(locale)
|
||||
setLocale(locale)
|
||||
})()
|
||||
|
||||
function switchTheme() {
|
||||
const mode = $theme.mode === "light" ? "dark" : "light"
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(async () => {
|
||||
$theme.mode = mode
|
||||
await tick()
|
||||
})
|
||||
} else {
|
||||
$theme.mode = mode
|
||||
}
|
||||
}
|
||||
|
||||
let languageSelect: HTMLSelectElement
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
@@ -13,22 +42,107 @@
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<input use:action={{title: $LL.profile.theme.COLOR_SCHEME()}} type="color" bind:value={$theme.color} />
|
||||
</li>
|
||||
<li>
|
||||
{#if $theme.mode === "light"}
|
||||
<button use:action={{title: $LL.profile.theme.DARK_MODE()}} class="icon" on:click={switchTheme}>
|
||||
dark_mode
|
||||
</button>
|
||||
{:else if $theme.mode === "dark"}
|
||||
<button use:action={{title: $LL.profile.theme.LIGHT_MODE()}} class="icon" on:click={switchTheme}>
|
||||
light_mode
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="icon"
|
||||
use:action={{title: $LL.profile.LANGUAGE()}}
|
||||
on:click={() => languageSelect.click()}
|
||||
>translate
|
||||
|
||||
<select bind:value={locale} bind:this={languageSelect}>
|
||||
{#each locales as code}
|
||||
<option value={code}>{code}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
select {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
inline-size: 20px;
|
||||
block-size: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
color: inherit;
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
&::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&::-webkit-color-swatch {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
ul:last-child {
|
||||
gap: 12px;
|
||||
|
||||
button {
|
||||
height: 24px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import {canAutoConnect} from "$lib/serial/device"
|
||||
import {browser} from "$app/environment"
|
||||
import {userPreferences} from "$lib/preferences"
|
||||
import {action} from "$lib/title"
|
||||
import LL from "../i18n/i18n-svelte"
|
||||
import Profile from "./Profile.svelte"
|
||||
import ConfigTabs from "./ConfigTabs.svelte"
|
||||
import EditActions from "./EditActions.svelte"
|
||||
|
||||
@@ -29,8 +29,18 @@
|
||||
|
||||
<div class="actions">
|
||||
{#if $canShare}
|
||||
<button transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</button>
|
||||
<button transition:fly={{x: -8}} class="icon" on:click={() => print()}>print</button>
|
||||
<button
|
||||
use:action={{title: $LL.share.TITLE()}}
|
||||
transition:fly={{x: -8}}
|
||||
class="icon"
|
||||
on:click={triggerShare}>share</button
|
||||
>
|
||||
<button
|
||||
use:action={{title: $LL.print.TITLE()}}
|
||||
transition:fly={{x: -8}}
|
||||
class="icon"
|
||||
on:click={() => print()}>print</button
|
||||
>
|
||||
<div transition:slide class="separator" />
|
||||
{/if}
|
||||
{#if import.meta.env.TAURI_FAMILY === undefined}
|
||||
@@ -39,90 +49,31 @@
|
||||
{/await}
|
||||
{/if}
|
||||
{#if $serialPort}
|
||||
<button title={$LL.backup.TITLE()} use:popup={BackupPopup} class="icon {$syncStatus}">
|
||||
{#if $syncStatus === "downloading"}
|
||||
backup
|
||||
{:else if $syncStatus === "uploading"}
|
||||
cloud_download
|
||||
{:else if $userPreferences.backup}
|
||||
cloud_done
|
||||
<button use:action={{title: $LL.backup.TITLE()}} use:popup={BackupPopup} class="icon {$syncStatus}">
|
||||
{#if $userPreferences.backup}
|
||||
history
|
||||
{:else}
|
||||
cloud_off
|
||||
history_toggle_off
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
bind:this={connectButton}
|
||||
title="Devices"
|
||||
use:action={{title: $LL.deviceManager.TITLE()}}
|
||||
use:popup={ConnectionPopup}
|
||||
class="icon connect"
|
||||
class:error={$serialPort === undefined}
|
||||
>
|
||||
cable
|
||||
</button>
|
||||
<button title={$LL.profile.TITLE()} use:popup={Profile} class="icon account">person</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style lang="scss">
|
||||
@keyframes sync {
|
||||
0% {
|
||||
scale: 1 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
85% {
|
||||
scale: 1 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
86% {
|
||||
scale: 1 1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
scale: 1 1;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.uploading::after,
|
||||
.downloading::after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform-origin: top;
|
||||
translate: -50% 0;
|
||||
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
|
||||
background: var(--md-sys-color-background);
|
||||
|
||||
animation: sync 1s linear infinite;
|
||||
}
|
||||
|
||||
.uploading::after {
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
.downloading.active::after,
|
||||
.uploading.active::after {
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.sync.downloading::after {
|
||||
top: 10px;
|
||||
transform-origin: bottom;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
margin-inline: 4px;
|
||||
background: var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
@@ -184,12 +135,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.icon.account {
|
||||
font-size: 32px;
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
background: var(--md-sys-color-secondary-container);
|
||||
}
|
||||
|
||||
:disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
|
||||
51
src/routes/PageTransition.svelte
Normal file
51
src/routes/PageTransition.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import {fly} from "svelte/transition"
|
||||
import {afterNavigate, beforeNavigate} from "$app/navigation"
|
||||
import {expoIn, expoOut, quadIn, quadOut} from "svelte/easing"
|
||||
|
||||
let inDirection = 0
|
||||
let outDirection = 0
|
||||
let outroEnd: undefined | (() => void) = undefined
|
||||
let animationDone: Promise<void>
|
||||
|
||||
let isNavigating = false
|
||||
|
||||
const routeOrder = ["/config/chords/", "/config/layout/", "/config/settings/"]
|
||||
|
||||
beforeNavigate(navigation => {
|
||||
const from = navigation.from?.url.pathname
|
||||
const to = navigation.to?.url.pathname
|
||||
isNavigating = true
|
||||
|
||||
if (!(from && to && routeOrder.includes(from) && routeOrder.includes(to))) {
|
||||
inDirection = 0
|
||||
outDirection = 0
|
||||
return
|
||||
}
|
||||
|
||||
const fromIndex = routeOrder.indexOf(from)
|
||||
const toIndex = routeOrder.indexOf(to)
|
||||
|
||||
inDirection = fromIndex > toIndex ? -1 : 1
|
||||
outDirection = fromIndex > toIndex ? 1 : -1
|
||||
|
||||
animationDone = new Promise(resolve => {
|
||||
outroEnd = resolve
|
||||
})
|
||||
})
|
||||
|
||||
afterNavigate(async () => {
|
||||
await animationDone
|
||||
isNavigating = false
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if !isNavigating}
|
||||
<main
|
||||
in:fly={{x: inDirection * 24, duration: 150, easing: expoOut}}
|
||||
out:fly={{x: outDirection * 24, duration: 150, easing: expoIn}}
|
||||
on:outroend={outroEnd}
|
||||
>
|
||||
<slot />
|
||||
</main>
|
||||
{/if}
|
||||
@@ -1,116 +0,0 @@
|
||||
<script lang="ts">
|
||||
import LL, {setLocale} from "../i18n/i18n-svelte"
|
||||
import {theme} from "$lib/preferences"
|
||||
import {tick} from "svelte"
|
||||
import {detectLocale, locales} from "../i18n/i18n-util"
|
||||
import {loadLocaleAsync} from "../i18n/i18n-util.async"
|
||||
import type {Locales} from "../i18n/i18n-types"
|
||||
|
||||
let locale = (localStorage.getItem("locale") as Locales) || detectLocale()
|
||||
$: (async () => {
|
||||
localStorage.setItem("locale", locale)
|
||||
await loadLocaleAsync(locale)
|
||||
setLocale(locale)
|
||||
})()
|
||||
|
||||
function switchTheme() {
|
||||
const mode = $theme.mode === "light" ? "dark" : "light"
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(async () => {
|
||||
$theme.mode = mode
|
||||
await tick()
|
||||
})
|
||||
} else {
|
||||
$theme.mode = mode
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<h2>{$LL.profile.TITLE()}</h2>
|
||||
<fieldset>
|
||||
<legend>
|
||||
<span class="icon">format_paint</span>
|
||||
{$LL.profile.theme.TITLE()}
|
||||
</legend>
|
||||
|
||||
<input title={$LL.profile.theme.COLOR_SCHEME()} type="color" bind:value={$theme.color} />
|
||||
<button
|
||||
title={$theme.mode === "light" ? $LL.profile.theme.LIGHT_MODE() : $LL.profile.theme.DARK_MODE()}
|
||||
class="icon"
|
||||
on:click={switchTheme}
|
||||
>
|
||||
{#if $theme.mode === "light"}
|
||||
light_mode
|
||||
{:else if $theme.mode === "dark"}
|
||||
dark_mode
|
||||
{:else}
|
||||
TODO
|
||||
{/if}
|
||||
</button>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>
|
||||
<span class="icon">translate</span>
|
||||
{$LL.profile.LANGUAGE()}
|
||||
</legend>
|
||||
{#each locales as code}
|
||||
<label>{code}<input bind:group={locale} type="radio" value={code} name="language" /></label>
|
||||
{/each}
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
h2 {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
section {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="color"] {
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
inline-size: 24px;
|
||||
block-size: 24px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
color: inherit;
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
&::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&::-webkit-color-swatch {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
68
src/routes/SyncOverlay.svelte
Normal file
68
src/routes/SyncOverlay.svelte
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import {syncProgress, syncStatus} from "$lib/serial/connection"
|
||||
import LL from "../i18n/i18n-svelte"
|
||||
|
||||
$: if (dialog) toggleDialog($syncStatus)
|
||||
|
||||
async function toggleDialog(status: "uploading" | "downloading" | string) {
|
||||
// debounce
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
if ($syncStatus !== status) return
|
||||
|
||||
if (!dialog.open && ($syncStatus === "uploading" || $syncStatus === "downloading")) {
|
||||
message = $syncStatus
|
||||
dialog.showModal()
|
||||
dialog.animate([{opacity: 0}, {opacity: 1}], {duration: 250, easing: "ease"})
|
||||
} else if (dialog.open) {
|
||||
const animation = dialog.animate([{opacity: 1}, {opacity: 0}], {duration: 250, easing: "ease"})
|
||||
animation.addEventListener("finish", () => {
|
||||
dialog.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let message: "downloading" | "uploading"
|
||||
let dialog: HTMLDialogElement
|
||||
</script>
|
||||
|
||||
<dialog bind:this={dialog}>
|
||||
{#if message === "downloading"}
|
||||
<h2>{$LL.sync.TITLE_READ()}</h2>
|
||||
{:else}
|
||||
<h2>{$LL.sync.TITLE_WRITE()}</h2>
|
||||
<p>{$LL.sync.DISCLAIMER_WRITE()}</p>
|
||||
{/if}
|
||||
<progress max={$syncProgress?.max ?? 1} value={$syncProgress?.current ?? 1}></progress>
|
||||
</dialog>
|
||||
|
||||
<style lang="scss">
|
||||
dialog::backdrop {
|
||||
background: rgba(0 0 0 / 70%);
|
||||
}
|
||||
|
||||
progress {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
background: var(--md-sys-color-background);
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
dialog {
|
||||
max-width: 14cm;
|
||||
padding: 2cm;
|
||||
|
||||
color: white;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES, KEYMAP_IDS} from "$lib/serial/keymap-codes"
|
||||
import {KEYMAP_IDS} from "$lib/serial/keymap-codes"
|
||||
import type {ChordInfo} from "$lib/undo-redo"
|
||||
import {changes, ChangeType} from "$lib/undo-redo"
|
||||
import {createEventDispatcher} from "svelte"
|
||||
import LL from "../../../i18n/i18n-svelte"
|
||||
import ActionString from "$lib/components/ActionString.svelte"
|
||||
|
||||
export let chord: ChordInfo | undefined = undefined
|
||||
|
||||
@@ -33,7 +34,7 @@
|
||||
changes.push({
|
||||
type: ChangeType.Chord,
|
||||
id: chord!.id,
|
||||
actions: [...pressedKeys],
|
||||
actions: [...pressedKeys].sort(),
|
||||
phrase: chord!.phrase,
|
||||
})
|
||||
return changes
|
||||
@@ -53,12 +54,7 @@
|
||||
{:else if !editing && !chord}
|
||||
<span>{$LL.configure.chords.NEW_CHORD()}</span>
|
||||
{/if}
|
||||
{#each editing ? [...pressedKeys].sort() : chord?.actions ?? [] as actionId}
|
||||
{@const {icon, id, code} = KEYMAP_CODES[actionId] ?? {code: actionId}}
|
||||
<kbd class:icon={!!icon}>
|
||||
{icon ?? id ?? `0x${code.toString(16)}`}
|
||||
</kbd>
|
||||
{/each}
|
||||
<ActionString display="keys" actions={editing ? [...pressedKeys].sort() : chord?.actions ?? []} />
|
||||
<sup>•</sup>
|
||||
</button>
|
||||
|
||||
@@ -87,12 +83,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
kbd {
|
||||
height: 24px;
|
||||
padding-block: auto;
|
||||
transition: color 250ms ease;
|
||||
}
|
||||
|
||||
button::after {
|
||||
content: "";
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES, KEYMAP_IDS, specialKeycodes} from "$lib/serial/keymap-codes"
|
||||
import {KEYMAP_IDS, specialKeycodes} from "$lib/serial/keymap-codes"
|
||||
import {tick} from "svelte"
|
||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
||||
import {changes, ChangeType} from "$lib/undo-redo"
|
||||
import type {ChordInfo} from "$lib/undo-redo"
|
||||
import {scale} from "svelte/transition"
|
||||
import ActionString from "$lib/components/ActionString.svelte"
|
||||
|
||||
export let chord: ChordInfo
|
||||
|
||||
@@ -149,14 +150,7 @@
|
||||
<div />
|
||||
<!-- placeholder for cursor placement -->
|
||||
{/if}
|
||||
{#each chord.phrase as actionId, i (`${actionId}:${i}`)}
|
||||
{@const {icon, id, code} = KEYMAP_CODES[actionId] ?? {code: actionId}}
|
||||
{#if !icon && id?.length === 1}
|
||||
<span>{id}</span>
|
||||
{:else}
|
||||
<kbd class:icon={!!icon}>{icon ?? id ?? `0x${code.toString(16)}`}</kbd>
|
||||
{/if}
|
||||
{/each}
|
||||
<ActionString actions={chord.phrase} />
|
||||
<sup>•</sup>
|
||||
</div>
|
||||
|
||||
@@ -203,14 +197,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
:not(.cursor) + kbd {
|
||||
margin-inline-start: 2px;
|
||||
}
|
||||
|
||||
kbd + * {
|
||||
margin-inline-start: 2px;
|
||||
}
|
||||
|
||||
[role="textbox"] {
|
||||
cursor: text;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user