mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Add Monaco Editor and implement code editor component
This commit is contained in:
@@ -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",
|
||||
|
||||
Generated
+8
@@ -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: {}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user