Add Monaco Editor and implement code editor component

This commit is contained in:
Torsten Dittmann
2025-04-10 18:54:55 +02:00
parent 93e1510102
commit 90bddf41b3
11 changed files with 426 additions and 1 deletions
+1
View File
@@ -39,6 +39,7 @@
"echarts": "^5.6.0",
"envfile": "^7.1.0",
"ignore": "^6.0.2",
"monaco-editor": "^0.52.2",
"nanoid": "^5.1.5",
"nanotar": "^0.1.1",
"plausible-tracker": "^0.3.9",
+8
View File
@@ -62,6 +62,9 @@ importers:
ignore:
specifier: ^6.0.2
version: 6.0.2
monaco-editor:
specifier: ^0.52.2
version: 0.52.2
nanoid:
specifier: ^5.1.5
version: 5.1.5
@@ -2679,6 +2682,9 @@ packages:
module-details-from-path@1.0.3:
resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==}
monaco-editor@0.52.2:
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@@ -6490,6 +6496,8 @@ snapshots:
module-details-from-path@1.0.3: {}
monaco-editor@0.52.2: {}
mri@1.2.0: {}
mrmime@2.0.1: {}
+2 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { Divider, Card, Typography, Layout, Button, Icon } from '@appwrite.io/pink-svelte';
import { Divider, Typography, Layout, Button, Icon } from '@appwrite.io/pink-svelte';
import {
IconArrowUp,
IconChevronDown,
@@ -21,6 +21,7 @@
<section
class="chat"
style:visibility={showChat ? 'visible' : 'hidden'}
style:width={$isSmallViewport ? 'calc(100vw - 16px)' : showChat ? `${width}px` : 0}
class:minimize-chat={minimizeChat}
class:is-visible={showChat}
@@ -0,0 +1,72 @@
import type { FileSystem } from '../filesystem';
import {
IconArrowLeft,
IconFolder,
IconFolderOpen,
IconJs,
IconSvelte,
IconCss3
} from '@appwrite.io/pink-icons-svelte';
export const icons = {
svelte: IconSvelte,
folder: IconFolder,
folderOpen: IconFolderOpen,
js: IconJs,
highlight: IconArrowLeft,
css: IconCss3
};
type Icon = 'svelte' | 'folder' | 'js' | 'css';
export type TreeItem = {
path: string;
title: string;
icon: Icon;
children?: TreeItem[];
};
function getIcon(filename: string): Icon {
if (filename.endsWith('.js')) return 'js';
if (filename.endsWith('.ts')) return 'js';
if (filename.endsWith('.html')) return 'svelte';
if (filename.endsWith('.css')) return 'css';
return 'folder';
}
export function treeFromFilesystem(fs: FileSystem): TreeItem[] {
const tree: TreeItem[] = [];
for (const path of fs) {
const parts = path.split('/');
let currentLevel = tree;
parts.forEach((part, index) => {
const existingItem = currentLevel.find((item) => item.title === part);
if (existingItem) {
if (!existingItem.children) {
existingItem.children = [];
}
currentLevel = existingItem.children;
} else {
const isFile = index === parts.length - 1;
const currentPath = parts.slice(0, index + 1).join('/');
const newItem: TreeItem = {
title: part,
icon: isFile ? getIcon(part) : 'folder',
children: isFile ? undefined : [],
path: currentPath
};
currentLevel.unshift(newItem);
if (!isFile) {
currentLevel = newItem.children!;
}
}
});
}
return tree;
}
@@ -0,0 +1,70 @@
<script lang="ts">
import { melt, type TreeView } from '@melt-ui/svelte';
import { getContext } from 'svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import { icons, type TreeItem } from '.';
export let items: TreeItem[];
export let level = 1;
const {
elements: { item, group },
helpers: { isExpanded, isSelected }
} = getContext<TreeView>('tree');
</script>
{#each items as { title, icon, children, path }, i}
{@const hasChildren = !!children?.length}
{@const isRoot = level === 1}
<li style:margin-inline-start={isRoot ? '' : '1rem'}>
<button
data-file={!hasChildren}
class:selected={$isSelected(path)}
use:melt={$item({
id: path,
hasChildren
})}>
{#if icon === 'folder' && hasChildren && $isExpanded(path)}
<Icon icon={icons['folderOpen']} />
{:else}
<Icon icon={icons[icon]} />
{/if}
<span>{title}</span>
</button>
{#if children}
<ul use:melt={$group({ id: path })}>
<svelte:self items={children} level={level + 1} />
</ul>
{/if}
</li>
{/each}
<style>
button {
display: flex;
width: 100%;
height: 2rem;
flex-direction: row;
align-items: center;
gap: 0.25rem;
user-select: none;
padding: var(--space-3) var(--space-4);
cursor: pointer;
border-radius: var(--border-radius-s);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
&.selected {
background: var(--bgcolor-neutral-tertiary);
}
&:hover:not(.selected) {
background: var(--bgcolor-neutral-secondary);
}
&:focus-visible {
outline: 2px solid #007aff;
outline-offset: -2px;
}
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import * as monaco from 'monaco-editor';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
let editorElement: HTMLDivElement;
let editor: monaco.editor.IStandaloneCodeEditor;
const code = `console.log("hello eldad");`;
export function loadCode(code: string, language: string) {
const model = monaco.editor.createModel(code, language);
editor.setModel(model);
}
onMount(async () => {
self.MonacoEnvironment = {
getWorker(_workerId: string, label: string) {
if (label === 'json') {
return new jsonWorker();
}
if (label === 'css' || label === 'scss' || label === 'less') {
return new cssWorker();
}
if (label === 'html') {
return new htmlWorker();
}
if (label === 'typescript' || label === 'javascript') {
return new tsWorker();
}
return new editorWorker();
}
};
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);
editor = monaco.editor.create(editorElement, {
automaticLayout: true,
theme: 'vs-light'
});
loadCode(code, 'typescript');
});
onDestroy(() => {
monaco?.editor.getModels().forEach((model) => model.dispose());
editor?.dispose();
});
</script>
<div class="editor" bind:this={editorElement}></div>
<style>
.editor {
height: 80vh;
}
</style>
@@ -0,0 +1,42 @@
<script lang="ts">
import { createTreeView } from '@melt-ui/svelte';
import { setContext } from 'svelte';
import { treeFromFilesystem } from './(filesystem)';
import type { FileSystem } from './filesystem';
import Tree from './(filesystem)/tree.svelte';
type Props = {
filesystem: FileSystem;
onopenfile?: (path: string) => void;
};
let { filesystem, onopenfile = null }: Props = $props();
const ctx = createTreeView();
setContext('tree', ctx);
const {
elements: { tree },
states: { selectedItem }
} = ctx;
const items = $derived(treeFromFilesystem(filesystem));
$effect(() => {
if ($selectedItem && onopenfile) {
if ($selectedItem.dataset['file'] === 'true') {
onopenfile($selectedItem.dataset['id']);
}
}
});
</script>
<ul {...$tree}>
<Tree {items} />
</ul>
<style>
ul {
display: flex;
flex-direction: column;
gap: 0;
}
</style>
+5
View File
@@ -0,0 +1,5 @@
import { writable } from 'svelte/store';
export type FileSystem = string[];
export const createFileSystem = (initial: FileSystem = []) => writable(initial);
@@ -0,0 +1,40 @@
<script lang="ts">
import Editor from '$lib/components/editor/editor.svelte';
import { createFileSystem } from '$lib/components/editor/filesystem';
import Filesystem from '$lib/components/editor/filesystem.svelte';
import { Layout } from '@appwrite.io/pink-svelte';
import html from '../../../../../app.html?raw';
import js from './app.js?raw';
import css from './app.css?raw';
const files = {
'app.html': html,
'src/app.js': js,
'src/app.css': css
};
const filesystem = createFileSystem(Object.keys(files));
let instance: Editor;
function getLanguageFromExtensions(path: string) {
if (path.endsWith('.js')) return 'javascript';
if (path.endsWith('.ts')) return 'typescript';
if (path.endsWith('.html')) return 'html';
if (path.endsWith('.css')) return 'css';
return 'folder';
}
function openFile(path: string) {
instance?.loadCode(files[path], getLanguageFromExtensions(path));
}
</script>
<Layout.Stack direction="row">
<div style:width="30%">
<Filesystem filesystem={$filesystem} onopenfile={openFile} />
</div>
<div style:width="70%">
<Editor bind:this={instance} />
</div>
</Layout.Stack>
@@ -0,0 +1,90 @@
body {
margin: 0;
font-size: 100%;
width: 100%;
}
[data-loading='true'] .page-loader {
pointer-events: initial;
visibility: visible;
opacity: 1;
}
.theme-dark .page-loader {
--p-page-loader-background-color: #000;
}
.theme-light .page-loader {
--p-page-loader-background-color: #fff;
}
.theme-light .page-loader {
background-color: #fff;
}
.page-loader {
pointer-events: none;
visibility: hidden;
opacity: 0;
z-index: 100000;
position: fixed;
height: 100%;
width: 100%;
background-color: var(--p-page-loader-background-color);
top: 0;
transition: all 0.2s ease-in-out;
}
.page-loader img {
visibility: hidden;
position: absolute;
max-width: 100%;
bottom: 60px;
left: 50%;
transform: translate(-50%, -50%);
}
.theme-dark .page-loader .logo-dark {
visibility: visible;
}
.theme-light .page-loader .logo-light {
visibility: visible;
}
.page-loader .animation {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%) translateZ(1px);
width: 140px;
height: 140px;
}
.page-loader .animation > div {
box-sizing: border-box;
display: block;
position: absolute;
width: 124px;
height: 124px;
margin: 10px;
border: 10px solid rgb(219, 26, 90);
border-radius: 50%;
animation: animation 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: rgb(219, 26, 90) transparent transparent transparent;
}
.page-loader .animation > div:nth-child(1) {
animation-delay: -0.45s;
}
.page-loader .animation > div:nth-child(2) {
animation-delay: -0.3s;
}
.page-loader .animation > div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes animation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@@ -0,0 +1,34 @@
import { fileURLToPath } from 'url';
import { build, loadEnv } from 'vite';
import kleur from 'kleur';
const { bold, yellow } = kleur;
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const env = loadEnv('production', __dirname, 'PUBLIC_');
function log(text = '', prefix = '') {
console.log(`${bold().green(`# ${prefix}`)}${text}`);
}
function logEnv(key, value, fallback = 'not set') {
log(value || yellow(fallback), `${key}: `);
}
function logDelimiter() {
console.log(bold().green('#'.repeat(80)));
}
async function main() {
logDelimiter();
log();
log(bold().magenta('APPWRITE CONSOLE'));
log();
logEnv('CONSOLE MODE', env?.PUBLIC_CONSOLE_MODE);
logEnv('APPWRITE ENDPOINT', env?.PUBLIC_APPWRITE_ENDPOINT, 'relative');
logEnv('GROWTH ENDPOINT', env?.PUBLIC_GROWTH_ENDPOINT);
log();
logDelimiter();
await build();
}
main();