mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-02-27 19:42:04 +00:00
feat: new chord editor prototype
This commit is contained in:
@@ -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) {
|
||||
|
||||
195
src/routes/(app)/config/cv2/+page.svelte
Normal file
195
src/routes/(app)/config/cv2/+page.svelte
Normal file
@@ -0,0 +1,195 @@
|
||||
<script lang="ts">
|
||||
import { chords } from "$lib/undo-redo";
|
||||
import { EditorView } from "codemirror";
|
||||
import { actionToValue } from "$lib/chord-editor/action-serializer";
|
||||
import { actionPlugin } from "$lib/chord-editor/action-plugin";
|
||||
import { delimPlugin } from "$lib/chord-editor/chord-delim-plugin";
|
||||
import {
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
highlightActiveLine,
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
} from "@codemirror/view";
|
||||
import { history, standardKeymap } from "@codemirror/commands";
|
||||
import "$lib/chord-editor/chords.grammar";
|
||||
import {
|
||||
chordHighlightStyle,
|
||||
chordLanguageSupport,
|
||||
} from "$lib/chord-editor/chords-grammar-plugin";
|
||||
import { syntaxHighlighting } from "@codemirror/language";
|
||||
import { autocompletion } from "@codemirror/autocomplete";
|
||||
import { persistentWritable } from "$lib/storage";
|
||||
import ActionList from "$lib/components/layout/ActionList.svelte";
|
||||
|
||||
const rawCode = persistentWritable("chord-editor-raw-code", false);
|
||||
const showEdits = persistentWritable("chord-editor-show-edits", true);
|
||||
let originalDoc = $derived(
|
||||
$chords
|
||||
.map(
|
||||
(chord) =>
|
||||
chord.actions
|
||||
.filter((it) => it !== 0)
|
||||
.map((it) => actionToValue(it))
|
||||
.join("") +
|
||||
"=>" +
|
||||
chord.phrase.map((it) => actionToValue(it)).join(""),
|
||||
)
|
||||
.join("\n"),
|
||||
);
|
||||
let editor: HTMLDivElement | undefined = $state(undefined);
|
||||
let view: EditorView;
|
||||
|
||||
$effect(() => {
|
||||
if (!editor) return;
|
||||
view = new EditorView({
|
||||
parent: editor,
|
||||
doc: originalDoc,
|
||||
extensions: [
|
||||
...($rawCode ? [] : [delimPlugin, actionPlugin]),
|
||||
chordLanguageSupport(),
|
||||
autocompletion({ icons: false, selectOnOpen: true }),
|
||||
history(),
|
||||
dropCursor(),
|
||||
syntaxHighlighting(chordHighlightStyle),
|
||||
highlightActiveLine(),
|
||||
drawSelection(),
|
||||
highlightSpecialChars(),
|
||||
keymap.of(standardKeymap),
|
||||
],
|
||||
});
|
||||
return () => view.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<label><input type="checkbox" bind:checked={$rawCode} />View as code</label>
|
||||
<label><input type="checkbox" bind:checked={$showEdits} />Show edits</label>
|
||||
|
||||
<div class="split">
|
||||
<div
|
||||
class="editor"
|
||||
class:hide-edits={!$showEdits}
|
||||
class:raw={$rawCode}
|
||||
bind:this={editor}
|
||||
></div>
|
||||
<ActionList />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.split {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
|
||||
> :global(:last-child) {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.editor:not(.raw) :global(.cm-line) {
|
||||
margin-inline: auto;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.editor :global(.cm-deletedChunk) {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.editor {
|
||||
min-width: 600px;
|
||||
height: 100%;
|
||||
font-size: 16px;
|
||||
|
||||
:global(.cm-tooltip) {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: var(--md-sys-color-surface-variant);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
:global(ul) {
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
:global(li[role="option"][aria-selected="true"]) {
|
||||
border-radius: 4px;
|
||||
background-color: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
}
|
||||
|
||||
:global(completion-section) {
|
||||
margin-block: 8px;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.change-button) {
|
||||
height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:global(.cm-deletedLineGutter) {
|
||||
background-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
:global(.cm-changedLineGutter) {
|
||||
background-color: var(--md-sys-color-success);
|
||||
}
|
||||
|
||||
:global(.cm-changedText) {
|
||||
background: linear-gradient(
|
||||
var(--md-sys-color-primary),
|
||||
var(--md-sys-color-primary)
|
||||
)
|
||||
bottom / 100% 1px no-repeat;
|
||||
}
|
||||
|
||||
:global(.cm-gutters) {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:global(.cm-editor) {
|
||||
outline: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.cm-line) {
|
||||
border-bottom: 1px solid transparent;
|
||||
line-height: 3em;
|
||||
}
|
||||
|
||||
:global(.cm-scroller) {
|
||||
overflow: auto;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
:global(.cm-cursor) {
|
||||
border-color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
:global(.cm-changedLine) {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--md-sys-color-primary) 5%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
:global(.cm-activeLine) {
|
||||
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
||||
/*background-color: color-mix(
|
||||
in srgb,
|
||||
var(--md-sys-color-surface-variant) 40%,
|
||||
transparent
|
||||
) !important;*/
|
||||
|
||||
&:not(.cm-changedLine) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.cm-selectionBackground) {
|
||||
background-color: var(--md-sys-color-surface-variant) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
71
src/routes/(app)/config/cv2/ChordEdit.svelte
Normal file
71
src/routes/(app)/config/cv2/ChordEdit.svelte
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import Action from "$lib/components/Action.svelte";
|
||||
import type { ChordInfo } from "$lib/undo-redo";
|
||||
import { onMount, tick } from "svelte";
|
||||
|
||||
let { chord }: { chord: ChordInfo } = $props();
|
||||
|
||||
let actualElements: HTMLDivElement | undefined = $state(undefined);
|
||||
let pseudoElements: HTMLDivElement | undefined = $state(undefined);
|
||||
|
||||
let widths: number[] = $state([]);
|
||||
|
||||
onMount(async () => {
|
||||
for (const letter of chord.phrase) {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = String.fromCodePoint(letter);
|
||||
pseudoElements?.appendChild(span);
|
||||
}
|
||||
|
||||
await tick();
|
||||
await tick();
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
update();
|
||||
});
|
||||
|
||||
function update() {
|
||||
console.log(document.getSelection());
|
||||
pseudoElements?.childNodes.forEach((node, index) => {
|
||||
if (node instanceof HTMLElement) {
|
||||
const range = document.createRange();
|
||||
const actual = actualElements?.childNodes[index];
|
||||
range.setStartBefore(actual);
|
||||
range.setEndAfter(actual);
|
||||
const rect = range.getBoundingClientRect();
|
||||
console.log(rect);
|
||||
node.style.width = rect.width + "px";
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="editor">
|
||||
<div class="visual" bind:this={actualElements}>
|
||||
{#each chord.phrase as action, index}
|
||||
<Action {action} display="inline-keys" />
|
||||
{/each}
|
||||
</div>
|
||||
<div contenteditable="true" bind:this={pseudoElements}></div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[contenteditable="true"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
> :global(span) {
|
||||
display: inline-block;
|
||||
background: red;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/routes/(app)/config/cv2/DropTarget.svelte
Normal file
34
src/routes/(app)/config/cv2/DropTarget.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
children,
|
||||
onofferdropbefore,
|
||||
onofferdropafter,
|
||||
onofferreplace,
|
||||
}: {
|
||||
children: Snippet;
|
||||
onofferdropbefore: () => void;
|
||||
onofferdropafter: () => void;
|
||||
onofferreplace: () => void;
|
||||
} = $props();
|
||||
|
||||
let element: HTMLSpanElement | undefined = $state(undefined);
|
||||
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="droptarget"
|
||||
bind:this={element}
|
||||
{ondrop}
|
||||
{ondragenter}
|
||||
{ondragleave}
|
||||
>
|
||||
{@render children()}
|
||||
</span>
|
||||
|
||||
<style lang="scss">
|
||||
.droptarget {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user