Moving files around
This commit is contained in:
169
src/lib/components/editor/Editor.svelte
Normal file
169
src/lib/components/editor/Editor.svelte
Normal file
@ -0,0 +1,169 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { EditorView } from 'codemirror';
|
||||
import { 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 type { EditorSettingsStore } from '../../stores/editorSettings';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
language?: 'javascript' | 'html' | 'css';
|
||||
onChange?: (value: string) => void;
|
||||
onExecute?: (code: string) => void;
|
||||
editorSettings: EditorSettingsStore;
|
||||
}
|
||||
|
||||
let {
|
||||
value = '',
|
||||
language = 'javascript',
|
||||
onChange,
|
||||
onExecute,
|
||||
editorSettings
|
||||
}: 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();
|
||||
|
||||
const evaluateKeymap = keymap.of([
|
||||
{
|
||||
key: 'Mod-e',
|
||||
run: (view) => {
|
||||
if (onExecute) {
|
||||
const code = view.state.doc.toString();
|
||||
onExecute(code);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
onMount(() => {
|
||||
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
|
||||
])
|
||||
];
|
||||
|
||||
const initSettings = $editorSettings;
|
||||
|
||||
editorView = new EditorView({
|
||||
doc: value,
|
||||
extensions: [
|
||||
...baseExtensions,
|
||||
languageExtensions[language],
|
||||
oneDark,
|
||||
evaluateKeymap,
|
||||
lineNumbersCompartment.of(initSettings.showLineNumbers ? lineNumbers() : []),
|
||||
lineWrappingCompartment.of(initSettings.enableLineWrapping ? EditorView.lineWrapping : []),
|
||||
vimCompartment.of(initSettings.vimMode ? vim() : []),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && onChange) {
|
||||
onChange(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
fontSize: `${initSettings.fontSize}px`,
|
||||
fontFamily: initSettings.fontFamily
|
||||
}
|
||||
})
|
||||
],
|
||||
parent: editorContainer
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const settings = $editorSettings;
|
||||
if (!editorView) return;
|
||||
|
||||
editorView.dispatch({
|
||||
effects: [
|
||||
lineNumbersCompartment.reconfigure(settings.showLineNumbers ? lineNumbers() : []),
|
||||
lineWrappingCompartment.reconfigure(settings.enableLineWrapping ? EditorView.lineWrapping : []),
|
||||
vimCompartment.reconfigure(settings.vimMode ? vim() : [])
|
||||
]
|
||||
});
|
||||
|
||||
editorView.dom.style.fontSize = `${settings.fontSize}px`;
|
||||
editorView.dom.style.fontFamily = settings.fontFamily;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (editorView) {
|
||||
editorView.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (editorView && value !== editorView.state.doc.toString()) {
|
||||
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>
|
||||
222
src/lib/components/editor/EditorSettings.svelte
Normal file
222
src/lib/components/editor/EditorSettings.svelte
Normal file
@ -0,0 +1,222 @@
|
||||
<script lang="ts">
|
||||
import { getAppContext } from '../../contexts/app-context';
|
||||
|
||||
const { editorSettings } = getAppContext();
|
||||
|
||||
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>
|
||||
118
src/lib/components/editor/EditorWithLogs.svelte
Normal file
118
src/lib/components/editor/EditorWithLogs.svelte
Normal file
@ -0,0 +1,118 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Editor from './Editor.svelte';
|
||||
import LogPanel from './LogPanel.svelte';
|
||||
import type { EditorSettingsStore } from '../../stores/editorSettings';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
language?: 'javascript' | 'html' | 'css';
|
||||
onChange?: (value: string) => void;
|
||||
onExecute?: (code: string) => void;
|
||||
logs?: string[];
|
||||
editorSettings: EditorSettingsStore;
|
||||
}
|
||||
|
||||
let {
|
||||
value = '',
|
||||
language = 'javascript',
|
||||
onChange,
|
||||
onExecute,
|
||||
logs = [],
|
||||
editorSettings
|
||||
}: Props = $props();
|
||||
|
||||
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);
|
||||
};
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div class="editor-with-logs">
|
||||
<div class="editor-section" style="height: {editorHeight}%;">
|
||||
<Editor
|
||||
{value}
|
||||
{language}
|
||||
{onChange}
|
||||
{onExecute}
|
||||
{editorSettings}
|
||||
/>
|
||||
</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>
|
||||
331
src/lib/components/editor/LogPanel.svelte
Normal file
331
src/lib/components/editor/LogPanel.svelte
Normal file
@ -0,0 +1,331 @@
|
||||
<script lang="ts">
|
||||
import { Copy, Trash2, Search, Pause, Play } from 'lucide-svelte';
|
||||
import { getAppContext } from '../../contexts/app-context';
|
||||
import type { LogEntry } from '../../csound';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
logs?: LogEntry[];
|
||||
}
|
||||
|
||||
let { logs = [] }: Props = $props();
|
||||
|
||||
const { csound } = getAppContext();
|
||||
|
||||
let searchQuery = $state('');
|
||||
let autoFollow = $state(true);
|
||||
let searchVisible = $state(false);
|
||||
let logContentEl: HTMLDivElement;
|
||||
let previousLogsLength = 0;
|
||||
|
||||
function fuzzyMatch(text: string, query: string): boolean {
|
||||
const textLower = text.toLowerCase();
|
||||
const queryLower = query.toLowerCase();
|
||||
let queryIndex = 0;
|
||||
|
||||
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
|
||||
if (textLower[i] === queryLower[queryIndex]) {
|
||||
queryIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return queryIndex === queryLower.length;
|
||||
}
|
||||
|
||||
const filteredLogs = $derived(
|
||||
searchQuery.trim() === ''
|
||||
? logs
|
||||
: logs.filter(log => fuzzyMatch(log.message, searchQuery))
|
||||
);
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
fractionalSecondDigits: 3
|
||||
});
|
||||
}
|
||||
|
||||
async function copyLogs() {
|
||||
if (logs.length === 0) return;
|
||||
|
||||
const logText = logs
|
||||
.map(log => `[${formatTime(log.timestamp)}] ${log.type.toUpperCase()}: ${log.message}`)
|
||||
.join('\n');
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(logText);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy logs:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
csound.clearLogs();
|
||||
}
|
||||
|
||||
function toggleAutoFollow() {
|
||||
autoFollow = !autoFollow;
|
||||
if (autoFollow) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
searchVisible = !searchVisible;
|
||||
if (!searchVisible) {
|
||||
searchQuery = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function scrollToBottom() {
|
||||
if (logContentEl) {
|
||||
await tick();
|
||||
logContentEl.scrollTop = logContentEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!logContentEl) return;
|
||||
|
||||
const isAtBottom =
|
||||
Math.abs(logContentEl.scrollHeight - logContentEl.scrollTop - logContentEl.clientHeight) < 5;
|
||||
|
||||
if (isAtBottom && !autoFollow) {
|
||||
autoFollow = true;
|
||||
} else if (!isAtBottom && autoFollow) {
|
||||
autoFollow = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (autoFollow && logs.length > previousLogsLength && logContentEl) {
|
||||
scrollToBottom();
|
||||
}
|
||||
previousLogsLength = logs.length;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="log-panel">
|
||||
<div class="log-header">
|
||||
<span class="log-title">Output</span>
|
||||
<div class="log-actions">
|
||||
<button
|
||||
class="action-button"
|
||||
class:search-active={searchVisible}
|
||||
onclick={toggleSearch}
|
||||
title={searchVisible ? 'Hide search' : 'Show search'}
|
||||
>
|
||||
<Search size={14} />
|
||||
</button>
|
||||
<button
|
||||
class="action-button"
|
||||
class:pause-active={!autoFollow}
|
||||
onclick={toggleAutoFollow}
|
||||
title={autoFollow ? 'Pause auto-follow' : 'Resume auto-follow'}
|
||||
>
|
||||
{#if autoFollow}
|
||||
<Pause size={14} />
|
||||
{:else}
|
||||
<Play size={14} />
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="action-button"
|
||||
onclick={copyLogs}
|
||||
disabled={logs.length === 0}
|
||||
title="Copy logs"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
<button
|
||||
class="action-button"
|
||||
onclick={clearLogs}
|
||||
disabled={logs.length === 0}
|
||||
title="Clear logs"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if searchVisible}
|
||||
<div class="search-bar">
|
||||
<Search size={14} class="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Search logs (fuzzy)..."
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="log-content" bind:this={logContentEl} onscroll={handleScroll}>
|
||||
{#if filteredLogs.length === 0 && searchQuery.trim() !== ''}
|
||||
<div class="empty-state">No matching logs found...</div>
|
||||
{:else if logs.length === 0}
|
||||
<div class="empty-state">No output yet...</div>
|
||||
{:else}
|
||||
{#each filteredLogs as log, i}
|
||||
<div class="log-entry" class:error={log.type === 'error'}>
|
||||
<span class="log-timestamp">{formatTime(log.timestamp)}</span>
|
||||
<span class="log-message">{log.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.log-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: #2a2a2a;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: #252525;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
}
|
||||
|
||||
.search-bar :global(.search-icon) {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background-color: #1a1a1a;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
border: 1px solid #3a3a3a;
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.log-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.log-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 0.25rem;
|
||||
background-color: transparent;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-button:hover:not(:disabled) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.action-button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button.pause-active {
|
||||
color: rgba(255, 200, 100, 0.9);
|
||||
background-color: rgba(255, 200, 100, 0.15);
|
||||
}
|
||||
|
||||
.action-button.pause-active:hover {
|
||||
color: rgba(255, 200, 100, 1);
|
||||
background-color: rgba(255, 200, 100, 0.25);
|
||||
}
|
||||
|
||||
.action-button.search-active {
|
||||
color: rgba(100, 200, 255, 0.9);
|
||||
background-color: rgba(100, 200, 255, 0.15);
|
||||
}
|
||||
|
||||
.action-button.search-active:hover {
|
||||
color: rgba(100, 200, 255, 1);
|
||||
background-color: rgba(100, 200, 255, 0.25);
|
||||
}
|
||||
|
||||
.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-entry.error {
|
||||
background-color: rgba(255, 0, 0, 0.1);
|
||||
border-left: 3px solid rgba(255, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.log-entry.error .log-message {
|
||||
color: rgba(255, 100, 100, 0.95);
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
min-width: 6rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user