mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2025-12-11 05:16:16 +00:00
feat: chord editing prototype
feat: lazy device connections feat: backup docs feat: chord library pagination
This commit is contained in:
64
docs/BACKUP.md
Normal file
64
docs/BACKUP.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Chara Backup Format, Version 1
|
||||||
|
|
||||||
|
JSON Schema files: TBD
|
||||||
|
|
||||||
|
Chara backups are serialized using JSON, in this general format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The presence of the key `charaVersion` uniquely identifies the JSON file as a chara backup file and serves
|
||||||
|
as a discriminator against other generic JSON files. This key is mandatory for that reason.
|
||||||
|
|
||||||
|
## Type `layout`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "layout",
|
||||||
|
"device": "one",
|
||||||
|
"layers": [[], [], []]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Devices at the current point in time may be identified as either `lite` or `one`, more to come in the future.
|
||||||
|
|
||||||
|
Layers are serialized as an array of `[layer1, layer2, layer3]` in the internal order of the key, each specifying
|
||||||
|
an action code. Action codes of `0` are considered unassigned.
|
||||||
|
|
||||||
|
## Type `chords`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "chords",
|
||||||
|
"chords": [
|
||||||
|
[
|
||||||
|
[1, 2, 3],
|
||||||
|
[3, 4, 5]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[6, 7, 8],
|
||||||
|
[9, 10, 11]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Chords are serialized using a key-value mapping of chord action codes to actions.
|
||||||
|
|
||||||
|
## Type `settings`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "settings",
|
||||||
|
"settings": [0, 1, 3, 6]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Settings are serialized as an array of the values in the way they appear on the device.
|
||||||
@@ -67,6 +67,8 @@ const config: IconsConfig = {
|
|||||||
"bolt",
|
"bolt",
|
||||||
"undo",
|
"undo",
|
||||||
"redo",
|
"redo",
|
||||||
|
"navigate_before",
|
||||||
|
"navigate_next",
|
||||||
],
|
],
|
||||||
codePoints: {
|
codePoints: {
|
||||||
speed: "e9e4",
|
speed: "e9e4",
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ const de = {
|
|||||||
INFO_BROWSER_SUFFIX: " sich bewusst dazu entschieden die API zu deaktivieren.",
|
INFO_BROWSER_SUFFIX: " sich bewusst dazu entschieden die API zu deaktivieren.",
|
||||||
DOWNLOAD_APP: "Desktop-app herunterladen",
|
DOWNLOAD_APP: "Desktop-app herunterladen",
|
||||||
},
|
},
|
||||||
|
changes: {
|
||||||
|
TITLE: "Änderungen anwenden",
|
||||||
|
CHORD_ADD: "{0} Akkord{{|e}} hinzugefügt",
|
||||||
|
CHORD_EDIT: "{0} Akkord{{|e}} bearbeitet",
|
||||||
|
CHORD_DELETE: "{0} Akkord{{|e}} entfernt",
|
||||||
|
SETTING_CHANGE: "{0} Einstellung{{|en}} geändert",
|
||||||
|
LAYOUT_CHANGE: "{0} Layout-belegung{{|en}} geändert",
|
||||||
|
},
|
||||||
configure: {
|
configure: {
|
||||||
chords: {
|
chords: {
|
||||||
TITLE: "Akkorde",
|
TITLE: "Akkorde",
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ const en = {
|
|||||||
INFO_BROWSER_SUFFIX: ".",
|
INFO_BROWSER_SUFFIX: ".",
|
||||||
DOWNLOAD_APP: "Download the desktop app",
|
DOWNLOAD_APP: "Download the desktop app",
|
||||||
},
|
},
|
||||||
|
changes: {
|
||||||
|
TITLE: "Apply changes",
|
||||||
|
CHORD_ADD: "{0} chord{{|s}} added",
|
||||||
|
CHORD_EDIT: "{0} chord{{|s}} edited",
|
||||||
|
CHORD_DELETE: "{0} chord{{|s}} deleted",
|
||||||
|
SETTING_CHANGE: "{0} setting{{|s}} changed",
|
||||||
|
LAYOUT_CHANGE: "{0} layout key{{|s}} changed",
|
||||||
|
},
|
||||||
configure: {
|
configure: {
|
||||||
chords: {
|
chords: {
|
||||||
TITLE: "Chords",
|
TITLE: "Chords",
|
||||||
|
|||||||
113
src/lib/components/ActionStringEdit.svelte
Normal file
113
src/lib/components/ActionStringEdit.svelte
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {KEYMAP_CODES, KEYMAP_IDS, specialKeycodes} from "$lib/serial/keymap-codes"
|
||||||
|
import {onDestroy, onMount} from "svelte"
|
||||||
|
import type ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
||||||
|
|
||||||
|
export let actions: number[]
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener("selectionchange", select)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
document.removeEventListener("selectionchange", select)
|
||||||
|
})
|
||||||
|
|
||||||
|
function input(event: KeyboardEvent) {
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowLeft":
|
||||||
|
case "ArrowRight":
|
||||||
|
case "Escape":
|
||||||
|
case "Tab": {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "Backspace": {
|
||||||
|
caretPosition--
|
||||||
|
if (caretPosition >= 0) actions = actions.toSpliced(caretPosition, 1)
|
||||||
|
else caretPosition = 0
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "Delete": {
|
||||||
|
if (caretPosition < actions.length) actions = actions.toSpliced(caretPosition, 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (specialKeycodes.has(event.key)) {
|
||||||
|
actions = actions.toSpliced(caretPosition, 0, 32)
|
||||||
|
} else if (KEYMAP_IDS.has(event.key)) {
|
||||||
|
actions = actions.toSpliced(caretPosition, 0, KEYMAP_IDS.get(event.key)!.code)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
console.log(event.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function select() {
|
||||||
|
const selection = document.getSelection()
|
||||||
|
if (!selection || !selection.containsNode(field, true)) return
|
||||||
|
|
||||||
|
let node = selection.anchorNode?.parentNode
|
||||||
|
let i = 0
|
||||||
|
while (node) {
|
||||||
|
i++
|
||||||
|
node = node.previousSibling
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
const clonedRange = range.cloneRange()
|
||||||
|
clonedRange.selectNodeContents(field)
|
||||||
|
clonedRange.setEnd(range.endContainer, range.endOffset)
|
||||||
|
|
||||||
|
caretPosition = (i - 1) / 2 + clonedRange.endOffset
|
||||||
|
console.log(caretPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
let editDialog: ActionSelector
|
||||||
|
let caretPosition: number
|
||||||
|
let field: HTMLSpanElement
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:selectionchange={select} />
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={field}
|
||||||
|
contenteditable
|
||||||
|
on:keydown={input}
|
||||||
|
spellcheck="false"
|
||||||
|
on:select|preventDefault={select}
|
||||||
|
role="textbox"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
{#each actions as char}
|
||||||
|
{@const action = KEYMAP_CODES[char]}
|
||||||
|
{#if action?.id && /^\w$/.test(action.id)}
|
||||||
|
<span data-action={char}>{KEYMAP_CODES[char].id}</span>
|
||||||
|
{:else if action}
|
||||||
|
<kbd data-action={char} title={action.title} class:icon={!!action.icon}>{action.icon || action.id}</kbd>
|
||||||
|
{:else}
|
||||||
|
<kbd data-action={char}>{action}</kbd>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<!-- <kbd class="icon" style="background: red">abc</kbd> -->
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
kbd {
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-inline-end: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(kbd) + kbd {
|
||||||
|
margin-inline-start: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span[contenteditable]:focus-within {
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if title}
|
{#if title}
|
||||||
<p>{title}</p>
|
<p>{@html title}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if shortcut}
|
{#if shortcut}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const layout = persistentWritable<CharaLayout>(
|
|||||||
|
|
||||||
export interface Change {
|
export interface Change {
|
||||||
layout?: Record<number, Record<number, number>>
|
layout?: Record<number, Record<number, number>>
|
||||||
chords?: never
|
chords?: Array<Record<"delete" | "edit" | "add", Chord>>
|
||||||
settings?: Record<number, number>
|
settings?: Record<number, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export class CharaDevice {
|
|||||||
!manual && ports.length === 1
|
!manual && ports.length === 1
|
||||||
? ports[0]
|
? ports[0]
|
||||||
: await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]})
|
: await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]})
|
||||||
|
|
||||||
await this.port.open({baudRate: this.baudRate})
|
await this.port.open({baudRate: this.baudRate})
|
||||||
const info = this.port.getInfo()
|
const info = this.port.getInfo()
|
||||||
serialLog.update(it => {
|
serialLog.update(it => {
|
||||||
@@ -53,7 +54,27 @@ export class CharaDevice {
|
|||||||
})
|
})
|
||||||
return it
|
return it
|
||||||
})
|
})
|
||||||
|
await this.port.close()
|
||||||
|
|
||||||
|
const [version] = await this.send("VERSION")
|
||||||
|
this.version = version.split(".").map(Number) as [number, number, number]
|
||||||
|
const [company, device, chipset] = await this.send("ID")
|
||||||
|
this.company = company as "CHARACHORDER"
|
||||||
|
this.device = device as "ONE" | "LITE"
|
||||||
|
this.chipset = chipset as "M0" | "S2"
|
||||||
|
}
|
||||||
|
|
||||||
|
private async suspend() {
|
||||||
|
await this.reader.cancel()
|
||||||
|
await this.streamClosed.catch(() => {
|
||||||
|
/** noop */
|
||||||
|
})
|
||||||
|
this.reader.releaseLock()
|
||||||
|
await this.port.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async wake() {
|
||||||
|
await this.port.open({baudRate: this.baudRate})
|
||||||
const decoderStream = new TextDecoderStream()
|
const decoderStream = new TextDecoderStream()
|
||||||
this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, {
|
this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, {
|
||||||
signal: this.abortController1.signal,
|
signal: this.abortController1.signal,
|
||||||
@@ -64,13 +85,6 @@ export class CharaDevice {
|
|||||||
signal: this.abortController2.signal,
|
signal: this.abortController2.signal,
|
||||||
})
|
})
|
||||||
.getReader()
|
.getReader()
|
||||||
|
|
||||||
const [version] = await this.send("VERSION")
|
|
||||||
this.version = version.split(".").map(Number) as [number, number, number]
|
|
||||||
const [company, device, chipset] = await this.send("ID")
|
|
||||||
this.company = company as "CHARACHORDER"
|
|
||||||
this.device = device as "ONE" | "LITE"
|
|
||||||
this.chipset = chipset as "M0" | "S2"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async internalRead() {
|
private async internalRead() {
|
||||||
@@ -105,19 +119,9 @@ export class CharaDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async forget() {
|
async forget() {
|
||||||
await this.disconnect()
|
|
||||||
await this.port.forget()
|
await this.port.forget()
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect() {
|
|
||||||
await this.reader.cancel()
|
|
||||||
await this.streamClosed.catch(() => {
|
|
||||||
/** noop */
|
|
||||||
})
|
|
||||||
this.reader.releaseLock()
|
|
||||||
await this.port.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read/write to serial port
|
* Read/write to serial port
|
||||||
*/
|
*/
|
||||||
@@ -132,8 +136,10 @@ export class CharaDevice {
|
|||||||
const exec = new Promise<T>(async resolve => {
|
const exec = new Promise<T>(async resolve => {
|
||||||
let result!: T
|
let result!: T
|
||||||
try {
|
try {
|
||||||
|
await this.wake()
|
||||||
result = await callback(send, read)
|
result = await callback(send, read)
|
||||||
} finally {
|
} finally {
|
||||||
|
await this.suspend()
|
||||||
this.lock = undefined
|
this.lock = undefined
|
||||||
resolve(result)
|
resolve(result)
|
||||||
}
|
}
|
||||||
@@ -252,7 +258,6 @@ export class CharaDevice {
|
|||||||
*/
|
*/
|
||||||
async reboot() {
|
async reboot() {
|
||||||
await this.send("RST")
|
await this.send("RST")
|
||||||
await this.disconnect()
|
|
||||||
// TODO: reconnect
|
// TODO: reconnect
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +266,6 @@ export class CharaDevice {
|
|||||||
*/
|
*/
|
||||||
async bootloader() {
|
async bootloader() {
|
||||||
await this.send("RST BOOTLOADER")
|
await this.send("RST BOOTLOADER")
|
||||||
await this.disconnect()
|
|
||||||
// TODO: more...
|
// TODO: more...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,17 @@ export const KEYMAP_CODES: Record<number, KeyInfo> = Object.fromEntries(
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const KEYMAP_IDS: Map<string, KeyInfo> = new Map(
|
||||||
|
keymaps
|
||||||
|
.flatMap(category =>
|
||||||
|
Object.entries(category.actions).map(
|
||||||
|
([code, action]) => [action.id!, {...action, code: Number(code), category}] as const,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(([id]) => id !== undefined),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const specialKeycodes = new Map([
|
||||||
|
[" ", 32], // Space
|
||||||
|
])
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ export interface CharaLayoutFile extends CharaFile<"layout"> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CharaChordFile extends CharaFile<"chords"> {
|
export interface CharaChordFile extends CharaFile<"chords"> {
|
||||||
chords: [number[], number[]]
|
chords: [number[], number[]][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharaChordSettings extends CharaFile<"settings"> {
|
||||||
|
settings: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharaFiles = CharaLayoutFile | CharaChordFile
|
export type CharaFiles = CharaLayoutFile | CharaChordFile
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ button {
|
|||||||
transition: all 250ms ease;
|
transition: all 250ms ease;
|
||||||
|
|
||||||
&.icon {
|
&.icon {
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
padding-block: 0;
|
padding-block: 0;
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
@@ -41,6 +43,10 @@ button {
|
|||||||
color: var(--md-sys-color-on-primary);
|
color: var(--md-sys-color-on-primary);
|
||||||
background: var(--md-sys-color-primary);
|
background: var(--md-sys-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label:has(input):hover,
|
label:has(input):hover,
|
||||||
|
|||||||
@@ -40,8 +40,14 @@
|
|||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
<button use:action={{title: $LL.saveActions.SAVE(), shortcut: "ctrl+shift+s"}} class="icon">save</button>
|
<button use:action={{title: $LL.saveActions.SAVE(), shortcut: "ctrl+shift+s"}} class="icon">save</button>
|
||||||
{#if $changes.length !== 0}
|
{#if $changes.length !== 0}
|
||||||
<button class="click-me" transition:fly={{x: 8}} use:action={{shortcut: "ctrl+s"}}
|
<button
|
||||||
><span class="icon">bolt</span>{$LL.saveActions.APPLY()}</button
|
class="click-me"
|
||||||
|
transition:fly={{x: 8}}
|
||||||
|
on:click={apply}
|
||||||
|
use:action={{
|
||||||
|
title: $LL.changes.TITLE(),
|
||||||
|
shortcut: "ctrl+s",
|
||||||
|
}}><span class="icon">bolt</span>{$LL.saveActions.APPLY()}</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {chords} from "$lib/serial/connection"
|
import {changes, chords} from "$lib/serial/connection"
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||||
import Index from "flexsearch"
|
import Index from "flexsearch"
|
||||||
import type {Chord} from "$lib/serial/chord"
|
import type {Chord} from "$lib/serial/chord"
|
||||||
import LL from "../../../i18n/i18n-svelte"
|
import LL from "../../../i18n/i18n-svelte"
|
||||||
|
import {action} from "$lib/title"
|
||||||
|
import {onDestroy, onMount} from "svelte"
|
||||||
|
import ActionStringEdit from "$lib/components/ActionStringEdit.svelte"
|
||||||
|
|
||||||
|
const resultSize = 38
|
||||||
|
let results: HTMLElement
|
||||||
|
let pageSize: number
|
||||||
|
let resizeObserver: ResizeObserver
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
pageSize = Math.floor(results.clientHeight / resultSize)
|
||||||
|
})
|
||||||
|
pageSize = Math.floor(results.clientHeight / resultSize)
|
||||||
|
resizeObserver.observe(results)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
resizeObserver?.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
|
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
|
||||||
|
|
||||||
@@ -23,6 +43,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: items = searchFilter?.map(it => [$chords[it], it] as const) ?? $chords.map((it, i) => [it, i] as const)
|
$: items = searchFilter?.map(it => [$chords[it], it] as const) ?? $chords.map((it, i) => [it, i] as const)
|
||||||
|
$: lastPage = Math.ceil(items.length / pageSize) - 1
|
||||||
|
|
||||||
|
let page = 0
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -35,35 +58,32 @@
|
|||||||
placeholder={$LL.configure.chords.search.PLACEHOLDER($chords.length)}
|
placeholder={$LL.configure.chords.search.PLACEHOLDER($chords.length)}
|
||||||
on:input={search}
|
on:input={search}
|
||||||
/>
|
/>
|
||||||
|
<span>{page + 1} / {lastPage + 1}</span>
|
||||||
|
<button class="icon" on:click={() => (page = Math.max(page - 1, 0))} use:action={{shortcut: "ctrl+left"}}
|
||||||
|
>navigate_before</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="icon"
|
||||||
|
on:click={() => (page = Math.min(page + 1, lastPage))}
|
||||||
|
use:action={{shortcut: "ctrl+right"}}>navigate_next</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
<section bind:this={results}>
|
||||||
{#if searchIndex}
|
|
||||||
<input
|
|
||||||
on:input={search}
|
|
||||||
type="search"
|
|
||||||
|
|
||||||
/>
|
|
||||||
{/if}-->
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<table>
|
<table>
|
||||||
{#each items.slice(0, 50) as [{ phrase, actions }, i]}
|
{#each items.slice(page * pageSize, (page + 1) * pageSize) as [chord]}
|
||||||
<tr style="view-transition-name: chord-{i}">
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
{#each phrase as char}
|
<ActionStringEdit actions={chord.phrase} />
|
||||||
{KEYMAP_CODES[char].id}
|
|
||||||
{/each}
|
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
{#each actions as action}
|
<ActionStringEdit actions={chord.actions} />
|
||||||
{@const keyInfo = KEYMAP_CODES[action]}
|
</td>
|
||||||
{#if keyInfo}
|
<td class="table-buttons">
|
||||||
<abbr title={keyInfo.title} class:icon={!!keyInfo.icon}>{keyInfo.icon || keyInfo.id}</abbr>
|
<button class="icon compact">share</button>
|
||||||
{:else}
|
<button class="icon compact" on:click={() => $changes.push({chords: [{delete: chord}]})}
|
||||||
<pre>{action}</pre>
|
>delete</button
|
||||||
{/if}
|
>
|
||||||
{/each}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -98,16 +118,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
--scrollbar-color: var(--md-sys-color-surface-variant);
|
|
||||||
|
|
||||||
scrollbar-gutter: stable;
|
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
padding-inline: 8px;
|
padding-inline: 8px;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -143,10 +158,12 @@
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
.table-buttons {
|
||||||
display: flex;
|
opacity: 0;
|
||||||
gap: 4px;
|
transition: opacity 75ms ease;
|
||||||
align-items: stretch;
|
}
|
||||||
justify-content: flex-end;
|
|
||||||
|
tr:hover > .table-buttons {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user