OK
This commit is contained in:
258
src/lib/Editor.svelte
Normal file
258
src/lib/Editor.svelte
Normal file
@@ -0,0 +1,258 @@
|
||||
<script lang="ts">
|
||||
import { Code, Palette, X, Globe, Layers } from 'lucide-svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { EditorState, Compartment } from '@codemirror/state';
|
||||
import { EditorView, keymap } from '@codemirror/view';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
import { state as appState } from './state.svelte';
|
||||
import { createTheme } from './theme';
|
||||
import type { Item } from './types';
|
||||
|
||||
type EditMode = 'normal' | 'vim' | 'emacs';
|
||||
|
||||
const themeCompartment = new Compartment();
|
||||
const keymapCompartment = new Compartment();
|
||||
|
||||
let { mode = 'item', item, onClose }: { mode?: 'item' | 'global'; item?: Item; onClose: () => void } = $props();
|
||||
|
||||
let activeTab = $state<'html' | 'css' | 'shared' | 'app'>(mode === 'item' ? 'html' : 'shared');
|
||||
let editMode = $state<EditMode>('normal');
|
||||
|
||||
function getKeymapExtension(m: EditMode) {
|
||||
switch (m) {
|
||||
case 'vim':
|
||||
return vim();
|
||||
case 'emacs':
|
||||
return emacs();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function cycleEditMode() {
|
||||
const modes: EditMode[] = ['normal', 'vim', 'emacs'];
|
||||
const idx = modes.indexOf(editMode);
|
||||
editMode = modes[(idx + 1) % modes.length];
|
||||
}
|
||||
|
||||
let htmlContainer: HTMLDivElement;
|
||||
let cssContainer: HTMLDivElement;
|
||||
let sharedContainer: HTMLDivElement;
|
||||
let appContainer: HTMLDivElement;
|
||||
|
||||
let htmlEditor: EditorView | null = null;
|
||||
let cssEditor: EditorView | null = null;
|
||||
let sharedEditor: EditorView | null = null;
|
||||
let appEditor: EditorView | null = null;
|
||||
|
||||
function createItemEditor(
|
||||
container: HTMLDivElement,
|
||||
lang: 'html' | 'css',
|
||||
initialValue: string
|
||||
): EditorView {
|
||||
const extensions = [
|
||||
keymapCompartment.of(getKeymapExtension(editMode)),
|
||||
keymap.of(defaultKeymap),
|
||||
themeCompartment.of(createTheme()),
|
||||
lang === 'html' ? html() : css(),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && item) {
|
||||
const value = update.state.doc.toString();
|
||||
if (lang === 'html') {
|
||||
appState.updateItem(item.id, { html: value });
|
||||
} else {
|
||||
appState.updateItem(item.id, { css: value });
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
return new EditorView({
|
||||
state: EditorState.create({ doc: initialValue, extensions }),
|
||||
parent: container
|
||||
});
|
||||
}
|
||||
|
||||
function createGlobalEditor(
|
||||
container: HTMLDivElement,
|
||||
type: 'shared' | 'app',
|
||||
initialValue: string
|
||||
): EditorView {
|
||||
const extensions = [
|
||||
keymapCompartment.of(getKeymapExtension(editMode)),
|
||||
keymap.of(defaultKeymap),
|
||||
themeCompartment.of(createTheme()),
|
||||
css(),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
const value = update.state.doc.toString();
|
||||
if (type === 'shared') {
|
||||
appState.updateSharedCss(value);
|
||||
} else {
|
||||
appState.updateAppCss(value);
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
return new EditorView({
|
||||
state: EditorState.create({ doc: initialValue, extensions }),
|
||||
parent: container
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (mode === 'item' && item) {
|
||||
if (htmlContainer) {
|
||||
htmlEditor = createItemEditor(htmlContainer, 'html', item.html);
|
||||
}
|
||||
if (cssContainer) {
|
||||
cssEditor = createItemEditor(cssContainer, 'css', item.css);
|
||||
}
|
||||
}
|
||||
if (sharedContainer) {
|
||||
sharedEditor = createGlobalEditor(sharedContainer, 'shared', appState.manifest.sharedCss);
|
||||
}
|
||||
if (appContainer) {
|
||||
appEditor = createGlobalEditor(appContainer, 'app', appState.manifest.appCss);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
htmlEditor?.destroy();
|
||||
cssEditor?.destroy();
|
||||
sharedEditor?.destroy();
|
||||
appEditor?.destroy();
|
||||
});
|
||||
|
||||
let previousAppCss = $state(appState.manifest.appCss);
|
||||
|
||||
$effect(() => {
|
||||
const currentAppCss = appState.manifest.appCss;
|
||||
if (currentAppCss !== previousAppCss) {
|
||||
previousAppCss = currentAppCss;
|
||||
setTimeout(() => {
|
||||
const newTheme = createTheme();
|
||||
htmlEditor?.dispatch({ effects: themeCompartment.reconfigure(newTheme) });
|
||||
cssEditor?.dispatch({ effects: themeCompartment.reconfigure(newTheme) });
|
||||
sharedEditor?.dispatch({ effects: themeCompartment.reconfigure(newTheme) });
|
||||
appEditor?.dispatch({ effects: themeCompartment.reconfigure(newTheme) });
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const ext = getKeymapExtension(editMode);
|
||||
htmlEditor?.dispatch({ effects: keymapCompartment.reconfigure(ext) });
|
||||
cssEditor?.dispatch({ effects: keymapCompartment.reconfigure(ext) });
|
||||
sharedEditor?.dispatch({ effects: keymapCompartment.reconfigure(ext) });
|
||||
appEditor?.dispatch({ effects: keymapCompartment.reconfigure(ext) });
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="panel">
|
||||
<div class="header">
|
||||
<div class="tabs">
|
||||
{#if mode === 'item'}
|
||||
<button class:active={activeTab === 'html'} onclick={() => (activeTab = 'html')} title="HTML"><Code size={12} /></button>
|
||||
<button class:active={activeTab === 'css'} onclick={() => (activeTab = 'css')} title="Item CSS"><Palette size={12} /></button>
|
||||
{/if}
|
||||
<button class:active={activeTab === 'shared'} onclick={() => (activeTab = 'shared')} title="Shared CSS"><Layers size={12} /></button>
|
||||
<button class:active={activeTab === 'app'} onclick={() => (activeTab = 'app')} title="App CSS"><Globe size={12} /></button>
|
||||
</div>
|
||||
<button class="mode" onclick={cycleEditMode} title="Editor mode: {editMode}">{editMode}</button>
|
||||
<button class="close" onclick={onClose} title="Close"><X size={12} /></button>
|
||||
</div>
|
||||
<div class="editor-container">
|
||||
{#if mode === 'item'}
|
||||
<div class="editor" class:hidden={activeTab !== 'html'} bind:this={htmlContainer}></div>
|
||||
<div class="editor" class:hidden={activeTab !== 'css'} bind:this={cssContainer}></div>
|
||||
{/if}
|
||||
<div class="editor" class:hidden={activeTab !== 'shared'} bind:this={sharedContainer}></div>
|
||||
<div class="editor" class:hidden={activeTab !== 'app'} bind:this={appContainer}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--surface, #282c34);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid var(--border, #333);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background: var(--surface, #282c34);
|
||||
border-bottom: 1px solid var(--border, #333);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
padding: 6px 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-dim, #666);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabs button:hover {
|
||||
color: var(--text, #fff);
|
||||
}
|
||||
|
||||
.tabs button.active {
|
||||
color: var(--accent, #4a9eff);
|
||||
}
|
||||
|
||||
.mode {
|
||||
padding: 6px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-dim, #666);
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mode:hover {
|
||||
color: var(--text, #fff);
|
||||
}
|
||||
|
||||
.close {
|
||||
padding: 6px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-dim, #666);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: var(--text, #fff);
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.editor.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user