Installing basic UI
This commit is contained in:
@ -25,12 +25,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.19.0",
|
||||||
|
"@codemirror/commands": "^6.9.0",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"@codemirror/lang-html": "^6.4.11",
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
"@codemirror/lang-javascript": "^6.2.4",
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
"@codemirror/language": "^6.11.3",
|
||||||
|
"@codemirror/lint": "^6.9.0",
|
||||||
|
"@codemirror/search": "^6.5.11",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"@codemirror/view": "^6.38.6",
|
||||||
"@csound/browser": "7.0.0-beta11",
|
"@csound/browser": "7.0.0-beta11",
|
||||||
|
"@replit/codemirror-vim": "^6.3.0",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
|
"lucide-svelte": "^0.545.0",
|
||||||
"pako": "^2.1.0"
|
"pako": "^2.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
@ -11,6 +11,12 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@codemirror/autocomplete':
|
||||||
|
specifier: ^6.19.0
|
||||||
|
version: 6.19.0
|
||||||
|
'@codemirror/commands':
|
||||||
|
specifier: ^6.9.0
|
||||||
|
version: 6.9.0
|
||||||
'@codemirror/lang-css':
|
'@codemirror/lang-css':
|
||||||
specifier: ^6.3.1
|
specifier: ^6.3.1
|
||||||
version: 6.3.1
|
version: 6.3.1
|
||||||
@ -20,15 +26,36 @@ importers:
|
|||||||
'@codemirror/lang-javascript':
|
'@codemirror/lang-javascript':
|
||||||
specifier: ^6.2.4
|
specifier: ^6.2.4
|
||||||
version: 6.2.4
|
version: 6.2.4
|
||||||
|
'@codemirror/language':
|
||||||
|
specifier: ^6.11.3
|
||||||
|
version: 6.11.3
|
||||||
|
'@codemirror/lint':
|
||||||
|
specifier: ^6.9.0
|
||||||
|
version: 6.9.0
|
||||||
|
'@codemirror/search':
|
||||||
|
specifier: ^6.5.11
|
||||||
|
version: 6.5.11
|
||||||
|
'@codemirror/state':
|
||||||
|
specifier: ^6.5.2
|
||||||
|
version: 6.5.2
|
||||||
'@codemirror/theme-one-dark':
|
'@codemirror/theme-one-dark':
|
||||||
specifier: ^6.1.3
|
specifier: ^6.1.3
|
||||||
version: 6.1.3
|
version: 6.1.3
|
||||||
|
'@codemirror/view':
|
||||||
|
specifier: ^6.38.6
|
||||||
|
version: 6.38.6
|
||||||
'@csound/browser':
|
'@csound/browser':
|
||||||
specifier: 7.0.0-beta11
|
specifier: 7.0.0-beta11
|
||||||
version: 7.0.0-beta11
|
version: 7.0.0-beta11
|
||||||
|
'@replit/codemirror-vim':
|
||||||
|
specifier: ^6.3.0
|
||||||
|
version: 6.3.0(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)
|
||||||
codemirror:
|
codemirror:
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
|
lucide-svelte:
|
||||||
|
specifier: ^0.545.0
|
||||||
|
version: 0.545.0(svelte@5.39.13)
|
||||||
pako:
|
pako:
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
@ -156,6 +183,15 @@ packages:
|
|||||||
'@oxc-project/types@0.93.0':
|
'@oxc-project/types@0.93.0':
|
||||||
resolution: {integrity: sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==}
|
resolution: {integrity: sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==}
|
||||||
|
|
||||||
|
'@replit/codemirror-vim@6.3.0':
|
||||||
|
resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@codemirror/commands': 6.x.x
|
||||||
|
'@codemirror/language': 6.x.x
|
||||||
|
'@codemirror/search': 6.x.x
|
||||||
|
'@codemirror/state': 6.x.x
|
||||||
|
'@codemirror/view': 6.x.x
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.0-beta.41':
|
'@rolldown/binding-android-arm64@1.0.0-beta.41':
|
||||||
resolution: {integrity: sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ==}
|
resolution: {integrity: sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@ -253,14 +289,14 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@sveltejs/vite-plugin-svelte': ^6.0.0-next.0
|
'@sveltejs/vite-plugin-svelte': ^6.0.0-next.0
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
vite: ^6.3.0 || ^7.0.0
|
vite: npm:rolldown-vite@7.1.14
|
||||||
|
|
||||||
'@sveltejs/vite-plugin-svelte@6.2.1':
|
'@sveltejs/vite-plugin-svelte@6.2.1':
|
||||||
resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==}
|
resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==}
|
||||||
engines: {node: ^20.19 || ^22.12 || >=24}
|
engines: {node: ^20.19 || ^22.12 || >=24}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
vite: ^6.3.0 || ^7.0.0
|
vite: npm:rolldown-vite@7.1.14
|
||||||
|
|
||||||
'@tsconfig/svelte@5.0.5':
|
'@tsconfig/svelte@5.0.5':
|
||||||
resolution: {integrity: sha512-48fAnUjKye38FvMiNOj0J9I/4XlQQiZlpe9xaNPfe8vy2Y1hFBt8g1yqf2EGjVvHavo4jf2lC+TQyENCr4BJBQ==}
|
resolution: {integrity: sha512-48fAnUjKye38FvMiNOj0J9I/4XlQQiZlpe9xaNPfe8vy2Y1hFBt8g1yqf2EGjVvHavo4jf2lC+TQyENCr4BJBQ==}
|
||||||
@ -523,6 +559,11 @@ packages:
|
|||||||
locate-character@3.0.0:
|
locate-character@3.0.0:
|
||||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||||
|
|
||||||
|
lucide-svelte@0.545.0:
|
||||||
|
resolution: {integrity: sha512-lVza6hIAf1abADTU1ThyNp+M3bQZtzTGQ9EQZ2xo3n9ftkd4QcWbDB2ekP2MqRLTJpeLXX5gaFEgFZtbWCnyqg==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^3 || ^4 || ^5.0.0-next.42
|
||||||
|
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
||||||
|
|
||||||
@ -709,7 +750,7 @@ packages:
|
|||||||
vitefu@1.1.1:
|
vitefu@1.1.1:
|
||||||
resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==}
|
resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
|
vite: npm:rolldown-vite@7.1.14
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
@ -902,6 +943,14 @@ snapshots:
|
|||||||
|
|
||||||
'@oxc-project/types@0.93.0': {}
|
'@oxc-project/types@0.93.0': {}
|
||||||
|
|
||||||
|
'@replit/codemirror-vim@6.3.0(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/commands': 6.9.0
|
||||||
|
'@codemirror/language': 6.11.3
|
||||||
|
'@codemirror/search': 6.5.11
|
||||||
|
'@codemirror/state': 6.5.2
|
||||||
|
'@codemirror/view': 6.38.6
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.0-beta.41':
|
'@rolldown/binding-android-arm64@1.0.0-beta.41':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -1188,6 +1237,10 @@ snapshots:
|
|||||||
|
|
||||||
locate-character@3.0.0: {}
|
locate-character@3.0.0: {}
|
||||||
|
|
||||||
|
lucide-svelte@0.545.0(svelte@5.39.13):
|
||||||
|
dependencies:
|
||||||
|
svelte: 5.39.13
|
||||||
|
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|||||||
228
src/App.svelte
228
src/App.svelte
@ -1,47 +1,209 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import svelteLogo from './assets/svelte.svg'
|
import TopBar from './lib/TopBar.svelte';
|
||||||
import viteLogo from '/vite.svg'
|
import EditorWithLogs from './lib/EditorWithLogs.svelte';
|
||||||
import Counter from './lib/Counter.svelte'
|
import EditorSettings from './lib/EditorSettings.svelte';
|
||||||
|
import SidePanel from './lib/SidePanel.svelte';
|
||||||
|
import Popup from './lib/Popup.svelte';
|
||||||
|
import {
|
||||||
|
PanelLeftClose,
|
||||||
|
PanelLeftOpen,
|
||||||
|
PanelRightClose,
|
||||||
|
PanelRightOpen,
|
||||||
|
PanelBottomClose,
|
||||||
|
PanelBottomOpen,
|
||||||
|
LayoutGrid
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
|
let sidePanelVisible = $state(true);
|
||||||
|
let sidePanelPosition = $state<'left' | 'right' | 'bottom'>('right');
|
||||||
|
let popupVisible = $state(false);
|
||||||
|
let editorValue = $state('// Start coding here...\n');
|
||||||
|
let interpreterLogs = $state<string[]>([]);
|
||||||
|
|
||||||
|
let sidePanelRef: SidePanel;
|
||||||
|
let editorRef: EditorWithLogs;
|
||||||
|
|
||||||
|
function toggleSidePanel() {
|
||||||
|
sidePanelVisible = !sidePanelVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPopup() {
|
||||||
|
popupVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditorChange(value: string) {
|
||||||
|
editorValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cyclePanelPosition() {
|
||||||
|
if (sidePanelPosition === 'right') {
|
||||||
|
sidePanelPosition = 'left';
|
||||||
|
} else if (sidePanelPosition === 'left') {
|
||||||
|
sidePanelPosition = 'bottom';
|
||||||
|
} else {
|
||||||
|
sidePanelPosition = 'right';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelTabs = [
|
||||||
|
{
|
||||||
|
id: 'editor',
|
||||||
|
label: 'Editor',
|
||||||
|
content: editorTabContent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'files',
|
||||||
|
label: 'Files',
|
||||||
|
content: filesTabContent
|
||||||
|
}
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
{#snippet editorTabContent()}
|
||||||
<div>
|
<EditorSettings />
|
||||||
<a href="https://vite.dev" target="_blank" rel="noreferrer">
|
{/snippet}
|
||||||
<img src={viteLogo} class="logo" alt="Vite Logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://svelte.dev" target="_blank" rel="noreferrer">
|
|
||||||
<img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<h1>Vite + Svelte</h1>
|
|
||||||
|
|
||||||
<div class="card">
|
{#snippet filesTabContent()}
|
||||||
<Counter />
|
<h3>File Browser</h3>
|
||||||
|
<p>Your project files will be listed here.</p>
|
||||||
|
<ul>
|
||||||
|
<li>src/</li>
|
||||||
|
<li>├── App.svelte</li>
|
||||||
|
<li>├── main.ts</li>
|
||||||
|
<li>└── lib/</li>
|
||||||
|
</ul>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div class="app-container">
|
||||||
|
<TopBar title="oldboy">
|
||||||
|
<button onclick={toggleSidePanel} class="icon-button">
|
||||||
|
{#if sidePanelVisible}
|
||||||
|
{#if sidePanelPosition === 'left'}
|
||||||
|
<PanelLeftClose size={18} />
|
||||||
|
{:else if sidePanelPosition === 'right'}
|
||||||
|
<PanelRightClose size={18} />
|
||||||
|
{:else}
|
||||||
|
<PanelBottomClose size={18} />
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
{#if sidePanelPosition === 'left'}
|
||||||
|
<PanelLeftOpen size={18} />
|
||||||
|
{:else if sidePanelPosition === 'right'}
|
||||||
|
<PanelRightOpen size={18} />
|
||||||
|
{:else}
|
||||||
|
<PanelBottomOpen size={18} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<button onclick={cyclePanelPosition} class="icon-button" title="Change panel position">
|
||||||
|
<LayoutGrid size={18} />
|
||||||
|
</button>
|
||||||
|
</TopBar>
|
||||||
|
|
||||||
|
<div class="main-content" class:panel-bottom={sidePanelPosition === 'bottom'}>
|
||||||
|
{#if sidePanelPosition === 'left'}
|
||||||
|
<SidePanel
|
||||||
|
bind:this={sidePanelRef}
|
||||||
|
bind:visible={sidePanelVisible}
|
||||||
|
bind:position={sidePanelPosition}
|
||||||
|
tabs={panelTabs}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="editor-area">
|
||||||
|
<EditorWithLogs
|
||||||
|
bind:this={editorRef}
|
||||||
|
initialValue={editorValue}
|
||||||
|
language="javascript"
|
||||||
|
onChange={handleEditorChange}
|
||||||
|
logs={interpreterLogs}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
{#if sidePanelPosition === 'right'}
|
||||||
Check out <a href="https://github.com/sveltejs/kit#readme" target="_blank" rel="noreferrer">SvelteKit</a>, the official Svelte app framework powered by Vite!
|
<SidePanel
|
||||||
</p>
|
bind:this={sidePanelRef}
|
||||||
|
bind:visible={sidePanelVisible}
|
||||||
|
bind:position={sidePanelPosition}
|
||||||
|
tabs={panelTabs}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<p class="read-the-docs">
|
{#if sidePanelPosition === 'bottom'}
|
||||||
Click on the Vite and Svelte logos to learn more
|
<SidePanel
|
||||||
</p>
|
bind:this={sidePanelRef}
|
||||||
</main>
|
bind:visible={sidePanelVisible}
|
||||||
|
bind:position={sidePanelPosition}
|
||||||
|
initialWidth={200}
|
||||||
|
minWidth={100}
|
||||||
|
maxWidth={400}
|
||||||
|
tabs={panelTabs}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popup
|
||||||
|
bind:visible={popupVisible}
|
||||||
|
title="Example Popup"
|
||||||
|
x={200}
|
||||||
|
y={150}
|
||||||
|
width={500}
|
||||||
|
height={400}
|
||||||
|
>
|
||||||
|
<h3>This is a popup!</h3>
|
||||||
|
<p>You can drag it around by the header.</p>
|
||||||
|
<p>It stays on top of everything else.</p>
|
||||||
|
</Popup>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.logo {
|
.app-container {
|
||||||
height: 6em;
|
display: flex;
|
||||||
padding: 1.5em;
|
flex-direction: column;
|
||||||
will-change: filter;
|
height: 100vh;
|
||||||
transition: filter 300ms;
|
width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
.main-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.logo.svelte:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #ff3e00aa);
|
.main-content.panel-bottom {
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
.editor-area {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: #333;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
border: 1px solid #555;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:hover {
|
||||||
|
background-color: #444;
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
68
src/app.css
68
src/app.css
@ -1,9 +1,13 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color-scheme: light dark;
|
color-scheme: dark;
|
||||||
color: rgba(255, 255, 255, 0.87);
|
color: rgba(255, 255, 255, 0.87);
|
||||||
background-color: #242424;
|
background-color: #242424;
|
||||||
|
|
||||||
@ -13,41 +17,33 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #646cff;
|
color: #646cff;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #535bf2;
|
color: #535bf2;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
padding: 0.6em 1.2em;
|
padding: 0.6em 1.2em;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
@ -57,23 +53,13 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.25s;
|
transition: border-color 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
border-color: #646cff;
|
border-color: #646cff;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus,
|
button:focus,
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 2px solid #646cff;
|
||||||
}
|
outline-offset: 2px;
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
158
src/lib/Editor.svelte
Normal file
158
src/lib/Editor.svelte
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { EditorView, minimalSetup } from 'codemirror';
|
||||||
|
import { EditorState, Compartment } from '@codemirror/state';
|
||||||
|
import { lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap } from '@codemirror/view';
|
||||||
|
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||||
|
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||||
|
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||||
|
import { foldGutter, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language';
|
||||||
|
import { lintKeymap } from '@codemirror/lint';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { html } from '@codemirror/lang-html';
|
||||||
|
import { css } from '@codemirror/lang-css';
|
||||||
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
import { vim } from '@replit/codemirror-vim';
|
||||||
|
import { editorSettings } from './stores/editorSettings';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
initialValue?: string;
|
||||||
|
language?: 'javascript' | 'html' | 'css';
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
initialValue = '',
|
||||||
|
language = 'javascript',
|
||||||
|
onChange
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let editorContainer: HTMLDivElement;
|
||||||
|
let editorView: EditorView | null = null;
|
||||||
|
|
||||||
|
const languageExtensions = {
|
||||||
|
javascript: javascript(),
|
||||||
|
html: html(),
|
||||||
|
css: css()
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineNumbersCompartment = new Compartment();
|
||||||
|
const lineWrappingCompartment = new Compartment();
|
||||||
|
const vimCompartment = new Compartment();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const settings = $editorSettings;
|
||||||
|
|
||||||
|
const baseExtensions = [
|
||||||
|
highlightActiveLineGutter(),
|
||||||
|
highlightSpecialChars(),
|
||||||
|
history(),
|
||||||
|
foldGutter(),
|
||||||
|
drawSelection(),
|
||||||
|
dropCursor(),
|
||||||
|
indentOnInput(),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
|
bracketMatching(),
|
||||||
|
closeBrackets(),
|
||||||
|
autocompletion(),
|
||||||
|
rectangularSelection(),
|
||||||
|
crosshairCursor(),
|
||||||
|
highlightSelectionMatches(),
|
||||||
|
keymap.of([
|
||||||
|
...closeBracketsKeymap,
|
||||||
|
...defaultKeymap,
|
||||||
|
...searchKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
...foldKeymap,
|
||||||
|
...completionKeymap,
|
||||||
|
...lintKeymap
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
editorView = new EditorView({
|
||||||
|
doc: initialValue,
|
||||||
|
extensions: [
|
||||||
|
...baseExtensions,
|
||||||
|
languageExtensions[language],
|
||||||
|
oneDark,
|
||||||
|
lineNumbersCompartment.of(settings.showLineNumbers ? lineNumbers() : []),
|
||||||
|
lineWrappingCompartment.of(settings.enableLineWrapping ? EditorView.lineWrapping : []),
|
||||||
|
vimCompartment.of(settings.vimMode ? vim() : []),
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged && onChange) {
|
||||||
|
onChange(update.state.doc.toString());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
EditorView.theme({
|
||||||
|
'&': {
|
||||||
|
fontSize: `${settings.fontSize}px`,
|
||||||
|
fontFamily: settings.fontFamily
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
parent: editorContainer
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubscribe = editorSettings.subscribe((newSettings) => {
|
||||||
|
if (!editorView) return;
|
||||||
|
|
||||||
|
editorView.dispatch({
|
||||||
|
effects: [
|
||||||
|
lineNumbersCompartment.reconfigure(newSettings.showLineNumbers ? lineNumbers() : []),
|
||||||
|
lineWrappingCompartment.reconfigure(newSettings.enableLineWrapping ? EditorView.lineWrapping : []),
|
||||||
|
vimCompartment.reconfigure(newSettings.vimMode ? vim() : [])
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
editorView.dom.style.fontSize = `${newSettings.fontSize}px`;
|
||||||
|
editorView.dom.style.fontFamily = newSettings.fontFamily;
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (editorView) {
|
||||||
|
editorView.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getValue(): string {
|
||||||
|
return editorView?.state.doc.toString() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setValue(value: string): void {
|
||||||
|
if (editorView) {
|
||||||
|
editorView.dispatch({
|
||||||
|
changes: { from: 0, to: editorView.state.doc.length, insert: value }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="editor-wrapper">
|
||||||
|
<div bind:this={editorContainer} class="editor-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.editor-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.cm-editor) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.cm-scroller) {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
220
src/lib/EditorSettings.svelte
Normal file
220
src/lib/EditorSettings.svelte
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { editorSettings } from './stores/editorSettings';
|
||||||
|
|
||||||
|
let settings = $state($editorSettings);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
settings = $editorSettings;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateSetting(key: keyof typeof settings, value: any) {
|
||||||
|
editorSettings.updatePartial({ [key]: value });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="editor-settings">
|
||||||
|
<div class="setting">
|
||||||
|
<label>
|
||||||
|
<span class="label-text">Font Size: {settings.fontSize}px</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="10"
|
||||||
|
max="24"
|
||||||
|
step="1"
|
||||||
|
value={settings.fontSize}
|
||||||
|
oninput={(e) => updateSetting('fontSize', parseInt(e.currentTarget.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting">
|
||||||
|
<label>
|
||||||
|
<span class="label-text">Font Family</span>
|
||||||
|
<select
|
||||||
|
value={settings.fontFamily}
|
||||||
|
onchange={(e) => updateSetting('fontFamily', e.currentTarget.value)}
|
||||||
|
>
|
||||||
|
<option value="'Roboto Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">Roboto Mono</option>
|
||||||
|
<option value="'JetBrains Mono', 'Roboto Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">JetBrains Mono</option>
|
||||||
|
<option value="'Fira Code', 'Roboto Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">Fira Code</option>
|
||||||
|
<option value="Monaco, 'Roboto Mono', Consolas, 'Liberation Mono', 'Courier New', monospace">Monaco</option>
|
||||||
|
<option value="Consolas, 'Roboto Mono', Monaco, 'Liberation Mono', 'Courier New', monospace">Consolas</option>
|
||||||
|
<option value="'Courier New', 'Roboto Mono', Monaco, Consolas, monospace">Courier New</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting">
|
||||||
|
<label>
|
||||||
|
<span class="label-text">Tab Size: {settings.tabSize}</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="2"
|
||||||
|
max="8"
|
||||||
|
step="2"
|
||||||
|
value={settings.tabSize}
|
||||||
|
oninput={(e) => updateSetting('tabSize', parseInt(e.currentTarget.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxes">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.vimMode}
|
||||||
|
onchange={(e) => updateSetting('vimMode', e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
<span>Vim mode</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.showLineNumbers}
|
||||||
|
onchange={(e) => updateSetting('showLineNumbers', e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
<span>Display line numbers</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.enableLineWrapping}
|
||||||
|
onchange={(e) => updateSetting('enableLineWrapping', e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
<span>Enable line wrapping</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.editor-settings {
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #4b5563;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: #646cff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: #646cff;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]:hover {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb:hover {
|
||||||
|
background: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb:hover {
|
||||||
|
background: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 0.4rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: #4b5563;
|
||||||
|
border: 1px solid #6b7280;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:hover {
|
||||||
|
background-color: #6b7280;
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked {
|
||||||
|
background-color: #646cff;
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked:hover {
|
||||||
|
background-color: #818cf8;
|
||||||
|
border-color: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked::before {
|
||||||
|
content: '✓';
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
128
src/lib/EditorWithLogs.svelte
Normal file
128
src/lib/EditorWithLogs.svelte
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import Editor from './Editor.svelte';
|
||||||
|
import LogPanel from './LogPanel.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
initialValue?: string;
|
||||||
|
language?: 'javascript' | 'html' | 'css';
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
logs?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
initialValue = '',
|
||||||
|
language = 'javascript',
|
||||||
|
onChange,
|
||||||
|
logs = []
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let editorRef: Editor;
|
||||||
|
let logPanelRef: LogPanel;
|
||||||
|
|
||||||
|
let editorHeight = $state(70);
|
||||||
|
let isResizing = $state(false);
|
||||||
|
let startY = $state(0);
|
||||||
|
let startHeight = $state(0);
|
||||||
|
|
||||||
|
function handleResizeStart(e: MouseEvent) {
|
||||||
|
isResizing = true;
|
||||||
|
startY = e.clientY;
|
||||||
|
startHeight = editorHeight;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResizeMove(e: MouseEvent) {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
const container = document.querySelector('.editor-with-logs');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const containerHeight = container.clientHeight;
|
||||||
|
const deltaY = e.clientY - startY;
|
||||||
|
const deltaPercent = (deltaY / containerHeight) * 100;
|
||||||
|
const newHeight = Math.max(20, Math.min(80, startHeight + deltaPercent));
|
||||||
|
|
||||||
|
editorHeight = newHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResizeEnd() {
|
||||||
|
isResizing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener('mousemove', handleResizeMove);
|
||||||
|
document.addEventListener('mouseup', handleResizeEnd);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleResizeMove);
|
||||||
|
document.removeEventListener('mouseup', handleResizeEnd);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getValue(): string {
|
||||||
|
return editorRef?.getValue() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setValue(value: string): void {
|
||||||
|
editorRef?.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addLog(message: string): void {
|
||||||
|
logPanelRef?.addLog(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLogs(): void {
|
||||||
|
logPanelRef?.clearLogs();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="editor-with-logs">
|
||||||
|
<div class="editor-section" style="height: {editorHeight}%;">
|
||||||
|
<Editor
|
||||||
|
bind:this={editorRef}
|
||||||
|
{initialValue}
|
||||||
|
{language}
|
||||||
|
{onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="resize-divider" onmousedown={handleResizeStart}></div>
|
||||||
|
|
||||||
|
<div class="logs-section" style="height: {100 - editorHeight}%;">
|
||||||
|
<LogPanel bind:this={logPanelRef} {logs} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.editor-with-logs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-section {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-divider {
|
||||||
|
height: 4px;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
cursor: ns-resize;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-divider:hover,
|
||||||
|
.resize-divider:active {
|
||||||
|
background-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-section {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
76
src/lib/LogPanel.svelte
Normal file
76
src/lib/LogPanel.svelte
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
logs?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let { logs = [] }: Props = $props();
|
||||||
|
|
||||||
|
export function addLog(message: string) {
|
||||||
|
logs.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLogs() {
|
||||||
|
logs = [];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="log-panel">
|
||||||
|
<div class="log-content">
|
||||||
|
{#if logs.length === 0}
|
||||||
|
<div class="empty-state">No output yet...</div>
|
||||||
|
{:else}
|
||||||
|
{#each logs as log, i}
|
||||||
|
<div class="log-entry">
|
||||||
|
<span class="log-index">{i + 1}</span>
|
||||||
|
<span class="log-message">{log}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.log-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:hover {
|
||||||
|
background-color: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-index {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
min-width: 2rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
135
src/lib/Popup.svelte
Normal file
135
src/lib/Popup.svelte
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
title = 'Popup',
|
||||||
|
visible = $bindable(false),
|
||||||
|
x = 100,
|
||||||
|
y = 100,
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
onClose
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let isDragging = $state(false);
|
||||||
|
let dragStartX = $state(0);
|
||||||
|
let dragStartY = $state(0);
|
||||||
|
let popupX = $state(x);
|
||||||
|
let popupY = $state(y);
|
||||||
|
|
||||||
|
function handleMouseDown(e: MouseEvent) {
|
||||||
|
if ((e.target as HTMLElement).classList.contains('popup-header')) {
|
||||||
|
isDragging = true;
|
||||||
|
dragStartX = e.clientX - popupX;
|
||||||
|
dragStartY = e.clientY - popupY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(e: MouseEvent) {
|
||||||
|
if (isDragging) {
|
||||||
|
popupX = e.clientX - dragStartX;
|
||||||
|
popupY = e.clientY - dragStartY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp() {
|
||||||
|
isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
visible = false;
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<div
|
||||||
|
class="popup"
|
||||||
|
style="left: {popupX}px; top: {popupY}px; width: {width}px; height: {height}px;"
|
||||||
|
>
|
||||||
|
<div class="popup-header" onmousedown={handleMouseDown}>
|
||||||
|
<span class="popup-title">{title}</span>
|
||||||
|
<button class="close-button" onclick={handleClose}>×</button>
|
||||||
|
</div>
|
||||||
|
<div class="popup-content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popup {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: #252525;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
214
src/lib/SidePanel.svelte
Normal file
214
src/lib/SidePanel.svelte
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible?: boolean;
|
||||||
|
initialWidth?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
position?: 'left' | 'right' | 'bottom';
|
||||||
|
tabs?: Array<{ id: string; label: string; content: any }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
visible = $bindable(true),
|
||||||
|
initialWidth = 300,
|
||||||
|
minWidth = 200,
|
||||||
|
maxWidth = 600,
|
||||||
|
position = $bindable('right'),
|
||||||
|
tabs = []
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let width = $state(initialWidth);
|
||||||
|
let isResizing = $state(false);
|
||||||
|
let startPos = $state(0);
|
||||||
|
let startWidth = $state(0);
|
||||||
|
let activeTab = $state(tabs[0]?.id || '');
|
||||||
|
|
||||||
|
function handleResizeStart(e: MouseEvent) {
|
||||||
|
isResizing = true;
|
||||||
|
if (position === 'bottom') {
|
||||||
|
startPos = e.clientY;
|
||||||
|
} else {
|
||||||
|
startPos = e.clientX;
|
||||||
|
}
|
||||||
|
startWidth = width;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResizeMove(e: MouseEvent) {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
let delta = 0;
|
||||||
|
if (position === 'left') {
|
||||||
|
delta = e.clientX - startPos;
|
||||||
|
} else if (position === 'right') {
|
||||||
|
delta = startPos - e.clientX;
|
||||||
|
} else if (position === 'bottom') {
|
||||||
|
delta = startPos - e.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + delta));
|
||||||
|
width = newWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResizeEnd() {
|
||||||
|
isResizing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (tabs.length > 0 && !activeTab) {
|
||||||
|
activeTab = tabs[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleResizeMove);
|
||||||
|
document.addEventListener('mouseup', handleResizeEnd);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleResizeMove);
|
||||||
|
document.removeEventListener('mouseup', handleResizeEnd);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function toggle() {
|
||||||
|
visible = !visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPosition(newPosition: 'left' | 'right' | 'bottom') {
|
||||||
|
position = newPosition;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<div class="side-panel" class:left={position === 'left'} class:right={position === 'right'} class:bottom={position === 'bottom'} style="{position === 'bottom' ? 'height' : 'width'}: {width}px;">
|
||||||
|
<div class="resize-handle" onmousedown={handleResizeStart}></div>
|
||||||
|
|
||||||
|
{#if tabs.length > 0}
|
||||||
|
<div class="tabs">
|
||||||
|
{#each tabs as tab}
|
||||||
|
<button
|
||||||
|
class="tab"
|
||||||
|
class:active={activeTab === tab.id}
|
||||||
|
onclick={() => activeTab = tab.id}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="side-panel-content">
|
||||||
|
{#each tabs as tab}
|
||||||
|
{#if activeTab === tab.id}
|
||||||
|
<div class="tab-content">
|
||||||
|
{@render tab.content()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="side-panel-content">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.side-panel {
|
||||||
|
position: relative;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel.left,
|
||||||
|
.side-panel.right {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel.left {
|
||||||
|
border-right: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel.right {
|
||||||
|
border-left: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel.bottom {
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left .resize-handle {
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 4px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right .resize-handle {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 4px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom .resize-handle {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover,
|
||||||
|
.resize-handle:active {
|
||||||
|
background-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
border-bottom-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
51
src/lib/TopBar.svelte
Normal file
51
src/lib/TopBar.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Code2 } from 'lucide-svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
children?: import('svelte').Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { title = 'oldboy', children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="top-bar">
|
||||||
|
<div class="title-section">
|
||||||
|
<Code2 size={20} />
|
||||||
|
<span class="title">{title}</span>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
height: 48px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/lib/stores/editorSettings.ts
Normal file
50
src/lib/stores/editorSettings.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export interface EditorSettings {
|
||||||
|
fontSize: number;
|
||||||
|
fontFamily: string;
|
||||||
|
showLineNumbers: boolean;
|
||||||
|
enableLineWrapping: boolean;
|
||||||
|
tabSize: number;
|
||||||
|
vimMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSettings: EditorSettings = {
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: "'Roboto Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||||
|
showLineNumbers: true,
|
||||||
|
enableLineWrapping: false,
|
||||||
|
tabSize: 2,
|
||||||
|
vimMode: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function createEditorSettings() {
|
||||||
|
const stored = localStorage.getItem('editorSettings');
|
||||||
|
const initial = stored ? { ...defaultSettings, ...JSON.parse(stored) } : defaultSettings;
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable<EditorSettings>(initial);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
set: (value: EditorSettings) => {
|
||||||
|
localStorage.setItem('editorSettings', JSON.stringify(value));
|
||||||
|
set(value);
|
||||||
|
},
|
||||||
|
update: (updater: (value: EditorSettings) => EditorSettings) => {
|
||||||
|
update((current) => {
|
||||||
|
const newValue = updater(current);
|
||||||
|
localStorage.setItem('editorSettings', JSON.stringify(newValue));
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updatePartial: (partial: Partial<EditorSettings>) => {
|
||||||
|
update((current) => {
|
||||||
|
const newValue = { ...current, ...partial };
|
||||||
|
localStorage.setItem('editorSettings', JSON.stringify(newValue));
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const editorSettings = createEditorSettings();
|
||||||
Reference in New Issue
Block a user