From 1aff1703ac294a76e410058f8c02b11d0d06884a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Wed, 17 Dec 2025 17:34:32 +0100 Subject: [PATCH] feat: new chord editor prototype --- icons.config.js | 1 + package.json | 3 + pnpm-lock.yaml | 39 +- src/i18n/de/index.ts | 2 +- src/i18n/en/index.ts | 2 +- src/lib/chord-editor/action-plugin.ts | 103 +++++ src/lib/chord-editor/action-serializer.ts | 16 + src/lib/chord-editor/autocomplete.ts | 72 ++++ src/lib/chord-editor/changes-plugin.ts | 17 + src/lib/chord-editor/chord-delim-plugin.ts | 80 ++++ src/lib/chord-editor/chords-grammar-plugin.ts | 57 +++ src/lib/chord-editor/chords.grammar | 27 ++ src/lib/chord-editor/grammar.d.ts | 3 + src/lib/chord-editor/test.txt | 16 + src/lib/components/Action.svelte | 97 ++++- src/lib/components/layout/ActionList.svelte | 387 ++++++++++++++++++ .../components/layout/ActionSelector.svelte | 353 +--------------- src/lib/style/_kbd.scss | 9 +- src/routes/(app)/+layout.svelte | 14 +- src/routes/(app)/config/cv2/+page.svelte | 195 +++++++++ src/routes/(app)/config/cv2/ChordEdit.svelte | 71 ++++ src/routes/(app)/config/cv2/DropTarget.svelte | 34 ++ vite-plugin-lezer.ts | 19 + vite.config.ts | 2 + 24 files changed, 1242 insertions(+), 377 deletions(-) create mode 100644 src/lib/chord-editor/action-plugin.ts create mode 100644 src/lib/chord-editor/action-serializer.ts create mode 100644 src/lib/chord-editor/autocomplete.ts create mode 100644 src/lib/chord-editor/changes-plugin.ts create mode 100644 src/lib/chord-editor/chord-delim-plugin.ts create mode 100644 src/lib/chord-editor/chords-grammar-plugin.ts create mode 100644 src/lib/chord-editor/chords.grammar create mode 100644 src/lib/chord-editor/grammar.d.ts create mode 100644 src/lib/chord-editor/test.txt create mode 100644 src/lib/components/layout/ActionList.svelte create mode 100644 src/routes/(app)/config/cv2/+page.svelte create mode 100644 src/routes/(app)/config/cv2/ChordEdit.svelte create mode 100644 src/routes/(app)/config/cv2/DropTarget.svelte create mode 100644 vite-plugin-lezer.ts diff --git a/icons.config.js b/icons.config.js index 8d697954..3a30b796 100644 --- a/icons.config.js +++ b/icons.config.js @@ -96,6 +96,7 @@ const config = { "undo", "redo", "replay", + "clock_loader_80", "reply", "navigate_before", "navigate_next", diff --git a/package.json b/package.json index 1acebb24..0363f1be 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,14 @@ "@codemirror/commands": "^6.8.1", "@codemirror/lang-javascript": "^6.2.4", "@codemirror/language": "^6.11.2", + "@codemirror/merge": "^6.11.2", "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.38.1", "@fontsource-variable/material-symbols-rounded": "^5.2.17", "@fontsource-variable/noto-sans-mono": "^5.2.7", + "@lezer/generator": "^1.8.0", "@lezer/highlight": "^1.2.1", + "@lezer/lr": "^1.4.5", "@material/material-color-utilities": "^0.3.0", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b00f27f..952fee69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@codemirror/language': specifier: ^6.11.2 version: 6.11.2 + '@codemirror/merge': + specifier: ^6.11.2 + version: 6.11.2 '@codemirror/state': specifier: ^6.5.2 version: 6.5.2 @@ -32,9 +35,15 @@ importers: '@fontsource-variable/noto-sans-mono': specifier: ^5.2.7 version: 5.2.7 + '@lezer/generator': + specifier: ^1.8.0 + version: 1.8.0 '@lezer/highlight': specifier: ^1.2.1 version: 1.2.1 + '@lezer/lr': + specifier: ^1.4.5 + version: 1.4.5 '@material/material-color-utilities': specifier: ^0.3.0 version: 0.3.0 @@ -795,6 +804,9 @@ packages: '@codemirror/lint@6.8.1': resolution: {integrity: sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==} + '@codemirror/merge@6.11.2': + resolution: {integrity: sha512-NO5EJd2rLRbwVWLgMdhIntDIhfDtMOKYEZgqV5WnkNUS2oXOCVWLPjG/kgl/Jth2fGiOuG947bteqxP9nBXmMg==} + '@codemirror/search@6.5.6': resolution: {integrity: sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==} @@ -1075,14 +1087,18 @@ packages: '@lezer/common@1.2.1': resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==} + '@lezer/generator@1.8.0': + resolution: {integrity: sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg==} + hasBin: true + '@lezer/highlight@1.2.1': resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} '@lezer/javascript@1.4.17': resolution: {integrity: sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==} - '@lezer/lr@1.4.1': - resolution: {integrity: sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==} + '@lezer/lr@1.4.5': + resolution: {integrity: sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==} '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} @@ -5157,7 +5173,7 @@ snapshots: '@codemirror/view': 6.38.1 '@lezer/common': 1.2.1 '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.1 + '@lezer/lr': 1.4.5 style-mod: 4.1.2 '@codemirror/lint@6.8.1': @@ -5166,6 +5182,14 @@ snapshots: '@codemirror/view': 6.38.1 crelt: 1.0.6 + '@codemirror/merge@6.11.2': + dependencies: + '@codemirror/language': 6.11.2 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.1 + '@lezer/highlight': 1.2.1 + style-mod: 4.1.2 + '@codemirror/search@6.5.6': dependencies: '@codemirror/state': 6.5.2 @@ -5385,6 +5409,11 @@ snapshots: '@lezer/common@1.2.1': {} + '@lezer/generator@1.8.0': + dependencies: + '@lezer/common': 1.2.1 + '@lezer/lr': 1.4.5 + '@lezer/highlight@1.2.1': dependencies: '@lezer/common': 1.2.1 @@ -5393,9 +5422,9 @@ snapshots: dependencies: '@lezer/common': 1.2.1 '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.1 + '@lezer/lr': 1.4.5 - '@lezer/lr@1.4.1': + '@lezer/lr@1.4.5': dependencies: '@lezer/common': 1.2.1 diff --git a/src/i18n/de/index.ts b/src/i18n/de/index.ts index 3e1144ca..09e49ae0 100644 --- a/src/i18n/de/index.ts +++ b/src/i18n/de/index.ts @@ -6,7 +6,7 @@ const de = { saveActions: { UNDO: "Rückgängig (shift halten um alle Änderungen rückgängig zu machen)", REDO: "Wiederholen", - SAVE: "Speichern", + SAVE: "Anwended", }, update: { TITLE: "Gerät aktualisieren", diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 8c30c43e..73bc77f3 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -7,7 +7,7 @@ const en = { saveActions: { UNDO: "Undo (hold shift to undo all changes)", REDO: "Redo", - SAVE: "Save", + SAVE: "Apply", }, update: { TITLE: "Update your device", diff --git a/src/lib/chord-editor/action-plugin.ts b/src/lib/chord-editor/action-plugin.ts new file mode 100644 index 00000000..0514097f --- /dev/null +++ b/src/lib/chord-editor/action-plugin.ts @@ -0,0 +1,103 @@ +import { + Decoration, + EditorView, + ViewPlugin, + ViewUpdate, + WidgetType, +} from "@codemirror/view"; +import { mount, unmount } from "svelte"; +import Action from "$lib/components/Action.svelte"; +import { syntaxTree } from "@codemirror/language"; +import type { Range } from "@codemirror/state"; + +export class ActionWidget extends WidgetType { + component: {}; + element: HTMLElement; + + constructor(readonly id: string | number) { + super(); + this.id = id; + this.element = document.createElement("span"); + this.element.style.paddingInline = "2px"; + + this.component = mount(Action, { + target: this.element, + props: { action: id, display: "keys" }, + }); + } + + override eq(other: ActionWidget) { + return this.id == other.id; + } + + toDOM() { + return this.element; + } + + override ignoreEvent() { + return true; + } + + override destroy() { + unmount(this.component); + } +} + +function actionWidgets(view: EditorView) { + const widgets: Range[] = []; + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: (node) => { + if (node.name !== "ExplicitAction") return; + const value = + node.node.getChild("ActionId") ?? + node.node.getChild("HexNumber") ?? + node.node.getChild("DecimalNumber"); + if (!value) return; + if (!node.node.getChild("ExplicitDelimEnd")) { + return; + } + + const id = view.state.doc.sliceString(value.from, value.to); + let deco = Decoration.replace({ + widget: new ActionWidget( + value.name === "ActionId" ? id : parseInt(id), + ), + }); + widgets.push(deco.range(node.from, node.to)); + }, + }); + } + return Decoration.set(widgets); +} + +export const actionPlugin = ViewPlugin.fromClass( + class { + decorations = Decoration.none; + + constructor(view: EditorView) { + this.decorations = actionWidgets(view); + } + + update(update: ViewUpdate) { + if ( + update.docChanged || + update.viewportChanged || + syntaxTree(update.startState) != syntaxTree(update.state) + ) + this.decorations = actionWidgets(update.view); + } + }, + { + decorations(instance) { + return instance.decorations; + }, + provide(plugin) { + return EditorView.atomicRanges.of( + (view) => view.plugin(plugin)?.decorations ?? Decoration.none, + ); + }, + }, +); diff --git a/src/lib/chord-editor/action-serializer.ts b/src/lib/chord-editor/action-serializer.ts new file mode 100644 index 00000000..d2781d63 --- /dev/null +++ b/src/lib/chord-editor/action-serializer.ts @@ -0,0 +1,16 @@ +import { KEYMAP_CODES, type KeyInfo } from "$lib/serial/keymap-codes"; +import { get } from "svelte/store"; + +export function canUseIdAsString(info: KeyInfo): boolean { + return !!info.id && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(info.id); +} + +export function actionToValue(action: number | KeyInfo) { + const info = + typeof action === "number" ? get(KEYMAP_CODES).get(action) : action; + if (info && info.id?.length === 1) + return /^[<>\\\s]$/.test(info.id) ? `\\${info.id}` : info.id; + if (!info || !canUseIdAsString(info)) + return `<0x${(info?.code ?? action).toString(16).padStart(2, "0")}>`; + return `<${info.id}>`; +} diff --git a/src/lib/chord-editor/autocomplete.ts b/src/lib/chord-editor/autocomplete.ts new file mode 100644 index 00000000..abb796e5 --- /dev/null +++ b/src/lib/chord-editor/autocomplete.ts @@ -0,0 +1,72 @@ +import { KEYMAP_CATEGORIES, KEYMAP_CODES } from "$lib/serial/keymap-codes"; +import type { + Completion, + CompletionSection, + CompletionSource, +} from "@codemirror/autocomplete"; +import { derived, get } from "svelte/store"; +import { actionToValue, canUseIdAsString } from "./action-serializer"; + +const completionSections = derived( + KEYMAP_CATEGORIES, + (categories) => + new Map( + categories.map( + (category) => + [ + category, + { + name: category.name, + } satisfies CompletionSection, + ] as const, + ), + ), +); + +export const actionAutocompleteItems = derived( + [KEYMAP_CODES, completionSections], + ([codes, sections]) => + codes + .values() + .map((info) => { + const canUseId = canUseIdAsString(info); + const completionValue = + (canUseId && info.id) || + `0x${info.code.toString(16).padStart(2, "0")}`; + return { + label: + [ + canUseId || !info.id ? undefined : `"${info.id}"`, + info.title, + info.variant?.replace(/^[a-z]/g, (c) => c.toUpperCase()), + ] + .filter(Boolean) + .join(" ") || completionValue, + detail: actionToValue(info), + section: info.category ? sections.get(info.category) : undefined, + info: info.description, + type: "keyword", + apply: completionValue + ">", + } satisfies Completion; + }) + .filter( + (item) => typeof item.label === "string" && item.apply !== undefined, + ) + .toArray(), +); + +export const actionAutocomplete = ((context) => { + let word = context.tokenBefore([ + "ExplicitDelimStart", + "ActionId", + "HexNumber", + "DecimalNumber", + ]); + if (!word) return null; + console.log(get(actionAutocompleteItems)); + return { + from: word.type.name === "ExplicitDelimStart" ? word.to : word.from, + validFor: /^[] = []; + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: (node) => { + if (node.name !== "PhraseDelim") return; + let deco = Decoration.replace({ + widget: new DelimWidget(), + }); + widgets.push(deco.range(node.from, node.to)); + }, + }); + } + return Decoration.set(widgets); +} + +export const delimPlugin = ViewPlugin.fromClass( + class { + decorations = Decoration.none; + + constructor(view: EditorView) { + this.decorations = actionWidgets(view); + } + + update(update: ViewUpdate) { + if ( + update.docChanged || + update.viewportChanged || + syntaxTree(update.startState) != syntaxTree(update.state) + ) + this.decorations = actionWidgets(update.view); + } + }, + { + decorations(instance) { + return instance.decorations; + }, + provide(plugin) { + return EditorView.atomicRanges.of( + (view) => view.plugin(plugin)?.decorations ?? Decoration.none, + ); + }, + }, +); diff --git a/src/lib/chord-editor/chords-grammar-plugin.ts b/src/lib/chord-editor/chords-grammar-plugin.ts new file mode 100644 index 00000000..6d524be5 --- /dev/null +++ b/src/lib/chord-editor/chords-grammar-plugin.ts @@ -0,0 +1,57 @@ +import { parser } from "./chords.grammar"; +import { + LRLanguage, + LanguageSupport, + HighlightStyle, +} from "@codemirror/language"; +import { styleTags, tags } from "@lezer/highlight"; +import { actionAutocomplete } from "./autocomplete"; + +export const chordHighlightStyle = HighlightStyle.define([ + { + tag: tags.keyword, + paddingInline: "2px", + opacity: "0.5", + }, + { + tag: tags.className, + backgroundColor: + "color-mix(in srgb, var(--md-sys-color-surface-variant) 50%, transparent)", + borderRadius: "4px", + paddingInline: "4px", + marginInline: "-4px", + }, + { + tag: tags.integer, + color: "var(--md-sys-color-tertiary)", + }, + { + tag: tags.angleBracket, + opacity: "0.5", + }, + { tag: tags.modifier, opacity: "0.25" }, + { tag: tags.escape, color: "var(--md-sys-color-primary)" }, + { tag: tags.strong, fontWeight: "bold" }, +]); + +export const chordLanguage = LRLanguage.define({ + name: "chords", + parser: parser.configure({ + props: [ + styleTags({ + "PhraseDelim CompoundDelim": [tags.keyword, tags.strong], + "HexNumber DecimalNumber": [tags.className, tags.integer], + "ExplicitDelimStart ExplicitDelimEnd": tags.angleBracket, + ActionId: tags.className, + EscapedLetter: tags.escape, + Escape: [tags.escape, tags.modifier], + }), + ], + }), +}); + +export function chordLanguageSupport() { + return new LanguageSupport(chordLanguage, [ + chordLanguage.data.of({ autocomplete: actionAutocomplete }), + ]); +} diff --git a/src/lib/chord-editor/chords.grammar b/src/lib/chord-editor/chords.grammar new file mode 100644 index 00000000..c63d9952 --- /dev/null +++ b/src/lib/chord-editor/chords.grammar @@ -0,0 +1,27 @@ +@top Program { Chord* } + +ExplicitAction { ExplicitDelimStart (HexNumber | DecimalNumber | ActionId) ExplicitDelimEnd } +EscapedSingleAction { Escape EscapedLetter } +Action { SingleLetter | ExplicitAction | EscapedSingleAction } +ActionString { Action* } +ChordInput { (ActionString CompoundDelim)* ActionString } +ChordPhrase { ActionString } +Chord { ChordInput PhraseDelim ChordPhrase ChordDelim } + +@tokens { + @precedence {HexNumber, DecimalNumber} + @precedence {CompoundDelim, PhraseDelim, ExplicitDelimStart, ChordDelim, SingleLetter} + @precedence {EscapedLetter} + ExplicitDelimStart {"<"} + ExplicitDelimEnd {">"} + CompoundDelim {"+>"} + PhraseDelim {"=>"} + Escape { "\\" } + HexNumber { "0x" $[a-fA-F0-9]+ } + DecimalNumber { $[0-9]+ } + ActionId { $[a-zA-Z_]$[a-zA-Z0-9_]* } + SingleLetter { ![\\] } + EscapedLetter { ![] } + ChordDelim { ($[\n] | @eof) } +} + diff --git a/src/lib/chord-editor/grammar.d.ts b/src/lib/chord-editor/grammar.d.ts new file mode 100644 index 00000000..2cae081f --- /dev/null +++ b/src/lib/chord-editor/grammar.d.ts @@ -0,0 +1,3 @@ +declare module "*.grammar" { + export const parser: import("@lezer/lr").LRParser; +} diff --git a/src/lib/chord-editor/test.txt b/src/lib/chord-editor/test.txt new file mode 100644 index 00000000..dd377111 --- /dev/null +++ b/src/lib/chord-editor/test.txt @@ -0,0 +1,16 @@ +.= => => +;ims => <0x219> +-; => <0x23e>_<0x23e> +.;g => <0x23e>...<0x23e> +'dg => <0x23e>'<0x23e> +'gl => <0x23e>'ll<0x23e> +'ar => <0x23e>'re<0x23e> +'gs => <0x23e>'s<0x23e> +'ev => <0x23e>'ve<0x23e> +-; => <0x23e><0x223>-<0x223> +; => <0x23e><0x223><0x23d><0x223> +;g => <0x23e><0x223><0x223> +deg => <0x23e>ed<0x23e> +;gr => <0x23e>er<0x23e> +;es => <0x23e>es<0x23e> +;est => <0x23e>est<0x23e> diff --git a/src/lib/components/Action.svelte b/src/lib/components/Action.svelte index 281d4564..385acb75 100644 --- a/src/lib/components/Action.svelte +++ b/src/lib/components/Action.svelte @@ -1,5 +1,5 @@ {#snippet popover()} - {#if info.icon || info.display || !info.id} - <{info.id ?? `0x${info.code.toString(16)}`}> - {/if} - {#if info.title} - {info.title} - {/if} - {#if info.variant === "left"} - (Left) - {:else if info.variant === "right"} - (Right) - {/if} - {#if info.description} -
- {info.description} + {#if retrievedInfo} + {#if info.icon || info.display || !info.id} + <{info.id ?? `0x${info.code.toString(16)}`}> + {/if} + {#if info.title} + {info.title} + {/if} + {#if info.variant === "left"} + (Left) + {:else if info.variant === "right"} + (Right) + {/if} + {#if info.description} +
+ {info.description} + {/if} + {:else} + Unknown Action
+ {#if info.code > 1023} + This action cannot be translated and will be ingored. + {/if} {/if} {/snippet} @@ -51,6 +72,8 @@ class:icon={!!info.icon} class:left={info.variant === "left"} class:right={info.variant === "right"} + class:error={info.code > 1023} + class:warn={!retrievedInfo} {@attach withPopover && hasPopover ? actionTooltip(popover) : null} > {@render kbdText()} @@ -60,21 +83,30 @@ {#if !info.icon && dynamicMapping?.length === 1} 1023} + class:warn={!retrievedInfo} class:left={info.variant === "left"} class:right={info.variant === "right"}>{dynamicMapping} {:else if !info.icon && info.id?.length === 1} 1023} + class:warn={!retrievedInfo} class:left={info.variant === "left"} class:right={info.variant === "right"}>{info.id} {:else} 1023} {@attach hasPopover ? actionTooltip(popover) : null} > {@render kbdText()} @@ -93,7 +125,7 @@ {:else} {@render inlineKbdSnippet()} {/if} -{:else if display === "inline-keys"} +{:else if display === "inline-keys" || display === "inline-text"} {@render inlineKbdSnippet()} {/if} @@ -102,6 +134,23 @@ transition: color 250ms ease; padding-block: auto; height: 24px; + + &.in-text { + display: inline-flex; + vertical-align: middle; + margin-block: auto; + padding-block: revert; + } + } + + .warn:not(.error) { + border-color: var(--md-sys-color-error); + color: var(--md-sys-color-error); + } + + .error { + opacity: 0.6; + text-decoration: line-through; } .left { @@ -113,6 +162,10 @@ .inline-kbd { margin-inline-end: 2px; + + &.in-text.icon { + translate: 0 -4em; + } } :global(span) + .inline-kbd { diff --git a/src/lib/components/layout/ActionList.svelte b/src/lib/components/layout/ActionList.svelte new file mode 100644 index 00000000..ea5a474c --- /dev/null +++ b/src/lib/components/layout/ActionList.svelte @@ -0,0 +1,387 @@ + + +
+
+ + + {#if onclose} + + + {/if} +
+ {#if currentAction !== undefined} + + {#if nextAction} + + {/if} + {/if} +
    + {#if exact !== undefined} +
  • + Exact match + select(exact)} /> +
  • + {/if} + {#if !exact && code} + {#if code >= 2 ** 5 && code < 2 ** 13} +
  • + {:else} +
  • Action code is out of range
  • + {/if} + {/if} + {#each results as [category, actions] (category)} + {#if actions.length > 0} +
    +

    {category.name}

    +
    {category.description}
    +
      + {#each actions as action (action.code)} + + {/each} +
    +
    + {/if} + {/each} +
+
+ + diff --git a/src/lib/components/layout/ActionSelector.svelte b/src/lib/components/layout/ActionSelector.svelte index 6c134299..9ab7cd11 100644 --- a/src/lib/components/layout/ActionSelector.svelte +++ b/src/lib/components/layout/ActionSelector.svelte @@ -1,19 +1,5 @@ - - - - { if (event.target === event.currentTarget) onclose(); }} > -
-
- { - if (event.key === "Enter") { - select(exact); - } - }} - placeholder={$LL.actionSearch.PLACEHOLDER()} - /> - - -
- {#if currentAction !== undefined} - - {#if nextAction} - - {/if} - {/if} -
    - {#if exact !== undefined} -
  • - Exact match - select(exact)} /> -
  • - {/if} - {#if !exact && code} - {#if code >= 2 ** 5 && code < 2 ** 13} -
  • - {:else} -
  • Action code is out of range
  • - {/if} - {/if} - {#each results as [category, actions] (category)} - {#if actions.length > 0} -
    -

    {category.name}

    -
    {category.description}
    -
      - {#each actions as action (action.code)} - - {/each} -
    -
    - {/if} - {/each} -
-
+
diff --git a/src/lib/style/_kbd.scss b/src/lib/style/_kbd.scss index 93d0f13e..0ab2e5da 100644 --- a/src/lib/style/_kbd.scss +++ b/src/lib/style/_kbd.scss @@ -3,9 +3,14 @@ kbd { justify-content: center; align-items: center; margin-block: 6px; - - border: 1px solid currentcolor; border-radius: 4px; + + //border: 1px solid currentcolor; + background: color-mix( + in srgb, + var(--md-sys-color-surface-variant) 50%, + transparent + ); padding: 4px; height: 20px; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index cd905f26..faa0753d 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -52,9 +52,21 @@ onMount(async () => { theme.subscribe((it) => { - const theme = themeFromSourceColor(argbFromHex(it.color)); + const theme = themeFromSourceColor(argbFromHex(it.color), [ + { + name: "success", + value: argbFromHex("#4CAF50"), + blend: true, + }, + ]); const dark = it.mode === "dark"; // window.matchMedia("(prefers-color-scheme: dark)").matches applyTheme(theme, { target: document.body, dark }); + for (const custom of theme.customColors) { + document.body.style.setProperty( + `--md-sys-color-${custom.color.name}`, + `#${custom.value.toString(16).padStart(8, "0").substring(2)}`, + ); + } }); if (import.meta.env.TAURI_FAMILY === undefined) { diff --git a/src/routes/(app)/config/cv2/+page.svelte b/src/routes/(app)/config/cv2/+page.svelte new file mode 100644 index 00000000..77439fe9 --- /dev/null +++ b/src/routes/(app)/config/cv2/+page.svelte @@ -0,0 +1,195 @@ + + + + + +
+
+ +
+ + diff --git a/src/routes/(app)/config/cv2/ChordEdit.svelte b/src/routes/(app)/config/cv2/ChordEdit.svelte new file mode 100644 index 00000000..bd1a04cb --- /dev/null +++ b/src/routes/(app)/config/cv2/ChordEdit.svelte @@ -0,0 +1,71 @@ + + +
+
+ {#each chord.phrase as action, index} + + {/each} +
+
+
+ + diff --git a/src/routes/(app)/config/cv2/DropTarget.svelte b/src/routes/(app)/config/cv2/DropTarget.svelte new file mode 100644 index 00000000..06e62afc --- /dev/null +++ b/src/routes/(app)/config/cv2/DropTarget.svelte @@ -0,0 +1,34 @@ + + + + {@render children()} + + + diff --git a/vite-plugin-lezer.ts b/vite-plugin-lezer.ts new file mode 100644 index 00000000..50c45006 --- /dev/null +++ b/vite-plugin-lezer.ts @@ -0,0 +1,19 @@ +import { buildParserFile } from "@lezer/generator"; +import type { Plugin, Rollup } from "vite"; + +const fileRegex = /\.(grammar)$/; + +export function lezerGrammarPlugin() { + return { + name: "lezer-grammar", + transform(code, id) { + if (fileRegex.test(id)) { + return { + code: buildParserFile(code).parser, + map: null, + } satisfies Rollup.TransformResult; + } + return null; + }, + } satisfies Plugin; +} diff --git a/vite.config.ts b/vite.config.ts index 50122d27..ea231b96 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,7 @@ import { SvelteKitPWA } from "@vite-pwa/sveltekit"; import ViteYaml from "@modyfi/vite-plugin-yaml"; import { readFile } from "fs/promises"; import { fileURLToPath } from "url"; +import { lezerGrammarPlugin } from "./vite-plugin-lezer"; const isTauri = "TAURI_FAMILY" in process.env; console.info(isTauri ? "Building for Tauri" : "Building for PWA"); @@ -48,6 +49,7 @@ export default defineConfig({ plugins: [ ViteYaml(), sveltekit(), + lezerGrammarPlugin(), ...(isTauri ? [] : [