lilylet-live-editor 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy.yml +81 -0
- package/README.md +78 -0
- package/package.json +50 -0
- package/scripts/build-docs.js +523 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +11 -0
- package/src/lib/components/Editor.svelte +164 -0
- package/src/lib/components/Player.svelte +529 -0
- package/src/lib/components/Preview.svelte +496 -0
- package/src/lib/index.ts +8 -0
- package/src/lib/lilylet/highlight.ts +179 -0
- package/src/lib/lilylet/index.ts +93 -0
- package/src/lib/stores/editor.ts +119 -0
- package/src/lib/utils/share.ts +68 -0
- package/src/lib/verovio/toolkit.ts +83 -0
- package/src/modules.d.ts +41 -0
- package/src/routes/+layout.svelte +8 -0
- package/src/routes/+layout.ts +3 -0
- package/src/routes/+page.svelte +596 -0
- package/src/routes/markdown/+page.svelte +980 -0
- package/static/docs/lilylet-tutorial.md +932 -0
- package/static/js/.gitkeep +7 -0
- package/static/js/musicWidgetsBrowser.umd.min.js +2 -0
- package/static/soundfont/acoustic_grand_piano-mp3.js +93 -0
- package/static/soundfont/acoustic_grand_piano-ogg.js +93 -0
- package/svelte.config.js +22 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +15 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { browser } from '$app/environment';
|
|
4
|
+
import { base } from '$app/paths';
|
|
5
|
+
import Editor from '$lib/components/Editor.svelte';
|
|
6
|
+
import Preview from '$lib/components/Preview.svelte';
|
|
7
|
+
import { editorStore } from '$lib/stores/editor';
|
|
8
|
+
import { lilyletToMEI, musicXmlToLilylet, lilypondToLilylet } from '$lib/lilylet';
|
|
9
|
+
import { getStateFromUrl, copyShareUrl } from '$lib/utils/share';
|
|
10
|
+
import { initVerovio, getToolkit } from '$lib/verovio/toolkit';
|
|
11
|
+
|
|
12
|
+
let shareStatus: 'idle' | 'copied' | 'error' = 'idle';
|
|
13
|
+
let lastRenderedCode = '';
|
|
14
|
+
let lastRenderedWidth = 0;
|
|
15
|
+
let resizeDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
16
|
+
|
|
17
|
+
// Divider drag state
|
|
18
|
+
let isDragging = false;
|
|
19
|
+
let editorWidth = 40; // percentage
|
|
20
|
+
let mainElement: HTMLElement;
|
|
21
|
+
|
|
22
|
+
// Drag and drop state
|
|
23
|
+
let isDragOver = false;
|
|
24
|
+
|
|
25
|
+
// Render cancellation token to prevent out-of-order updates
|
|
26
|
+
let currentRenderId = 0;
|
|
27
|
+
|
|
28
|
+
// Watch for code changes and re-render (only when code actually changes)
|
|
29
|
+
$: if (browser && $editorStore.verovioReady && $editorStore.code && $editorStore.code !== lastRenderedCode) {
|
|
30
|
+
lastRenderedCode = $editorStore.code;
|
|
31
|
+
lastRenderedWidth = $editorStore.previewWidth;
|
|
32
|
+
renderScore($editorStore.code);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Watch for width changes and re-render with debounce
|
|
36
|
+
$: if (browser && $editorStore.verovioReady && $editorStore.svg && $editorStore.previewWidth !== lastRenderedWidth) {
|
|
37
|
+
if (resizeDebounceTimer) clearTimeout(resizeDebounceTimer);
|
|
38
|
+
resizeDebounceTimer = setTimeout(() => {
|
|
39
|
+
lastRenderedWidth = $editorStore.previewWidth;
|
|
40
|
+
renderScore($editorStore.code);
|
|
41
|
+
}, 150);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function handleShare() {
|
|
45
|
+
const success = await copyShareUrl({ code: $editorStore.code });
|
|
46
|
+
shareStatus = success ? 'copied' : 'error';
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
shareStatus = 'idle';
|
|
49
|
+
}, 2000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function setupVerovio() {
|
|
53
|
+
try {
|
|
54
|
+
await initVerovio();
|
|
55
|
+
editorStore.setVerovioReady(true);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error('Failed to initialize Verovio:', err);
|
|
58
|
+
editorStore.setError('Failed to initialize Verovio: ' + String(err));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function renderScore(code: string) {
|
|
63
|
+
const toolkit = getToolkit();
|
|
64
|
+
if (!toolkit) return;
|
|
65
|
+
|
|
66
|
+
// Increment render ID and capture for this render
|
|
67
|
+
const renderId = ++currentRenderId;
|
|
68
|
+
|
|
69
|
+
editorStore.setRendering(true);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Convert Lilylet to MEI
|
|
73
|
+
const result = await lilyletToMEI(code);
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
// Check if this render is still current
|
|
76
|
+
if (renderId !== currentRenderId) return;
|
|
77
|
+
editorStore.setError('Failed to parse Lilylet code');
|
|
78
|
+
editorStore.addLog('error', `Lilylet to MEI conversion failed: ${result.error}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { mei, measureCount, staffCount } = result;
|
|
83
|
+
|
|
84
|
+
// Check if this render is still current before updating store
|
|
85
|
+
if (renderId !== currentRenderId) return;
|
|
86
|
+
editorStore.setMEI(mei);
|
|
87
|
+
|
|
88
|
+
// Calculate pageWidth based on container - account for padding
|
|
89
|
+
const effectiveWidth = Math.max(400, $editorStore.previewWidth - 80);
|
|
90
|
+
// Verovio scale 40 means 40% of default size, pageWidth is in abstract units
|
|
91
|
+
// At scale 40, approximately 2.5 abstract units = 1 pixel
|
|
92
|
+
const pageWidthUnits = Math.round(effectiveWidth * 2.5);
|
|
93
|
+
|
|
94
|
+
// Calculate pageHeight based on measure count and staff count
|
|
95
|
+
// ~20 measures fit in one standard page (height ~2000 units at scale 40)
|
|
96
|
+
const basePageHeight = 2000;
|
|
97
|
+
const measuresPerPage = 20;
|
|
98
|
+
const pageHeight = Math.max(basePageHeight, Math.ceil(measureCount / measuresPerPage) * basePageHeight) * 2 * staffCount;
|
|
99
|
+
|
|
100
|
+
toolkit.setOptions({
|
|
101
|
+
scale: 40,
|
|
102
|
+
adjustPageHeight: true,
|
|
103
|
+
pageHeight,
|
|
104
|
+
pageWidth: pageWidthUnits
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Render with Verovio
|
|
108
|
+
const success = toolkit.loadData(mei);
|
|
109
|
+
if (!success) {
|
|
110
|
+
if (renderId !== currentRenderId) return;
|
|
111
|
+
editorStore.setError('Verovio failed to load MEI data');
|
|
112
|
+
editorStore.addLog('error', 'Verovio failed to load MEI data');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const pageCount = toolkit.getPageCount();
|
|
117
|
+
const svg = toolkit.renderToSVG(1);
|
|
118
|
+
|
|
119
|
+
// Final check before committing results
|
|
120
|
+
if (renderId !== currentRenderId) return;
|
|
121
|
+
editorStore.setSVG(svg, pageCount);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// Only set error if this render is still current
|
|
124
|
+
if (renderId === currentRenderId) {
|
|
125
|
+
console.error('Render error:', err);
|
|
126
|
+
editorStore.setError(String(err));
|
|
127
|
+
editorStore.addLog('error', `Render error: ${String(err)}`);
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
// Only clear rendering flag if this is the current render
|
|
131
|
+
if (renderId === currentRenderId) {
|
|
132
|
+
editorStore.setRendering(false);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Drag and drop handlers
|
|
138
|
+
function handleDragOver(e: DragEvent) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
isDragOver = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function handleDragLeave(e: DragEvent) {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
isDragOver = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function handleFileDrop(file: File) {
|
|
149
|
+
const fileName = file.name.toLowerCase();
|
|
150
|
+
|
|
151
|
+
// Check file type by extension
|
|
152
|
+
const isLilypond = fileName.endsWith('.ly') || fileName.endsWith('.ily');
|
|
153
|
+
const isMusicXml = fileName.endsWith('.musicxml') || fileName.endsWith('.mxl') ||
|
|
154
|
+
(fileName.endsWith('.xml') && !fileName.endsWith('.mei.xml'));
|
|
155
|
+
|
|
156
|
+
if (!isLilypond && !isMusicXml) {
|
|
157
|
+
editorStore.addLog('warning', `Unsupported file type: ${file.name}. Supported formats: .ly, .musicxml, .xml`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const content = await file.text();
|
|
163
|
+
editorStore.addLog('info', `Converting ${file.name}...`);
|
|
164
|
+
|
|
165
|
+
if (isLilypond) {
|
|
166
|
+
const result = lilypondToLilylet(content);
|
|
167
|
+
if (result.success) {
|
|
168
|
+
editorStore.setCode(result.data);
|
|
169
|
+
editorStore.addLog('info', `Successfully converted ${file.name} to Lilylet`);
|
|
170
|
+
} else {
|
|
171
|
+
editorStore.addLog('error', result.error);
|
|
172
|
+
}
|
|
173
|
+
} else if (isMusicXml) {
|
|
174
|
+
const result = musicXmlToLilylet(content);
|
|
175
|
+
if (result.success) {
|
|
176
|
+
editorStore.setCode(result.data);
|
|
177
|
+
editorStore.addLog('info', `Successfully converted ${file.name} to Lilylet`);
|
|
178
|
+
} else {
|
|
179
|
+
editorStore.addLog('error', result.error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
184
|
+
editorStore.addLog('error', `Failed to read file: ${errorMessage}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function handleDrop(e: DragEvent) {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
isDragOver = false;
|
|
191
|
+
|
|
192
|
+
const files = e.dataTransfer?.files;
|
|
193
|
+
if (!files || files.length === 0) return;
|
|
194
|
+
|
|
195
|
+
await handleFileDrop(files[0]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function handleDividerMouseDown(e: MouseEvent) {
|
|
199
|
+
isDragging = true;
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function handleMouseMove(e: MouseEvent) {
|
|
204
|
+
if (!isDragging || !mainElement) return;
|
|
205
|
+
const rect = mainElement.getBoundingClientRect();
|
|
206
|
+
const newWidth = ((e.clientX - rect.left) / rect.width) * 100;
|
|
207
|
+
// Clamp between 20% and 80%
|
|
208
|
+
editorWidth = Math.max(20, Math.min(80, newWidth));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleMouseUp() {
|
|
212
|
+
isDragging = false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
onMount(() => {
|
|
216
|
+
if (browser) {
|
|
217
|
+
// Load state from URL if present
|
|
218
|
+
const urlState = getStateFromUrl();
|
|
219
|
+
if (urlState?.code) {
|
|
220
|
+
editorStore.setCode(urlState.code);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
setupVerovio();
|
|
224
|
+
|
|
225
|
+
// Add global mouse event listeners for dragging
|
|
226
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
227
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
228
|
+
|
|
229
|
+
return () => {
|
|
230
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
231
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<svelte:head>
|
|
238
|
+
<title>Lilylet Live Editor</title>
|
|
239
|
+
</svelte:head>
|
|
240
|
+
|
|
241
|
+
<div class="app">
|
|
242
|
+
<header>
|
|
243
|
+
<h1>Lilylet Live Editor</h1>
|
|
244
|
+
<nav>
|
|
245
|
+
<span class="current">Editor</span>
|
|
246
|
+
<a href="{base}/markdown">Markdown Demo</a>
|
|
247
|
+
<a href="{base}/docs/lilylet-tutorial.html" target="_blank">Tutorial</a>
|
|
248
|
+
</nav>
|
|
249
|
+
<div class="header-actions">
|
|
250
|
+
<button class="share-btn" on:click={handleShare}>
|
|
251
|
+
{#if shareStatus === 'copied'}
|
|
252
|
+
Copied!
|
|
253
|
+
{:else if shareStatus === 'error'}
|
|
254
|
+
Error
|
|
255
|
+
{:else}
|
|
256
|
+
Share
|
|
257
|
+
{/if}
|
|
258
|
+
</button>
|
|
259
|
+
<span class="status">
|
|
260
|
+
{#if !$editorStore.verovioReady}
|
|
261
|
+
Loading Verovio...
|
|
262
|
+
{:else}
|
|
263
|
+
Ready
|
|
264
|
+
{/if}
|
|
265
|
+
</span>
|
|
266
|
+
</div>
|
|
267
|
+
</header>
|
|
268
|
+
|
|
269
|
+
<main
|
|
270
|
+
bind:this={mainElement}
|
|
271
|
+
class:dragging={isDragging}
|
|
272
|
+
class:drag-over={isDragOver}
|
|
273
|
+
on:dragover={handleDragOver}
|
|
274
|
+
on:dragleave={handleDragLeave}
|
|
275
|
+
on:drop={handleDrop}
|
|
276
|
+
>
|
|
277
|
+
<div class="pane editor-pane" style="flex: 0 0 {editorWidth}%">
|
|
278
|
+
<Editor on:filedrop={(e) => { isDragOver = false; handleFileDrop(e.detail); }} />
|
|
279
|
+
</div>
|
|
280
|
+
<div class="divider" on:mousedown={handleDividerMouseDown}></div>
|
|
281
|
+
<div class="pane preview-pane">
|
|
282
|
+
<Preview />
|
|
283
|
+
</div>
|
|
284
|
+
{#if isDragOver}
|
|
285
|
+
<div class="drop-overlay">
|
|
286
|
+
<div class="drop-message">
|
|
287
|
+
Drop LilyPond (.ly) or MusicXML (.musicxml, .xml) file to convert
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
{/if}
|
|
291
|
+
</main>
|
|
292
|
+
|
|
293
|
+
<!-- Collapsible Log Area -->
|
|
294
|
+
<div class="log-area" class:expanded={$editorStore.logsExpanded}>
|
|
295
|
+
<button class="log-toggle" on:click={() => editorStore.setLogsExpanded(!$editorStore.logsExpanded)}>
|
|
296
|
+
<span class="toggle-icon">{$editorStore.logsExpanded ? '▼' : '▶'}</span>
|
|
297
|
+
Logs ({$editorStore.logs.length})
|
|
298
|
+
{#if $editorStore.logs.filter(l => l.level === 'error').length > 0}
|
|
299
|
+
<span class="error-badge">{$editorStore.logs.filter(l => l.level === 'error').length} errors</span>
|
|
300
|
+
{/if}
|
|
301
|
+
</button>
|
|
302
|
+
{#if $editorStore.logsExpanded}
|
|
303
|
+
<div class="log-content">
|
|
304
|
+
<div class="log-actions">
|
|
305
|
+
<button class="clear-btn" on:click={() => editorStore.clearLogs()}>Clear</button>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="log-entries">
|
|
308
|
+
{#each $editorStore.logs as log}
|
|
309
|
+
<div class="log-entry log-{log.level}">
|
|
310
|
+
<span class="log-time">{log.timestamp.toLocaleTimeString()}</span>
|
|
311
|
+
<span class="log-level">[{log.level.toUpperCase()}]</span>
|
|
312
|
+
<span class="log-message">{log.message}</span>
|
|
313
|
+
</div>
|
|
314
|
+
{/each}
|
|
315
|
+
{#if $editorStore.logs.length === 0}
|
|
316
|
+
<div class="log-empty">No logs yet</div>
|
|
317
|
+
{/if}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
{/if}
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<style>
|
|
325
|
+
:global(body) {
|
|
326
|
+
margin: 0;
|
|
327
|
+
padding: 0;
|
|
328
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
329
|
+
background: #1e1e1e;
|
|
330
|
+
color: #d4d4d4;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
:global(*) {
|
|
334
|
+
box-sizing: border-box;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.app {
|
|
338
|
+
display: flex;
|
|
339
|
+
flex-direction: column;
|
|
340
|
+
height: 100vh;
|
|
341
|
+
overflow: hidden;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
header {
|
|
345
|
+
display: flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
justify-content: space-between;
|
|
348
|
+
padding: 8px 16px;
|
|
349
|
+
background: #333333;
|
|
350
|
+
border-bottom: 1px solid #454545;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
h1 {
|
|
354
|
+
margin: 0;
|
|
355
|
+
font-size: 18px;
|
|
356
|
+
font-weight: 600;
|
|
357
|
+
color: #ffffff;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
nav {
|
|
361
|
+
display: flex;
|
|
362
|
+
gap: 16px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
nav a {
|
|
366
|
+
color: #858585;
|
|
367
|
+
text-decoration: none;
|
|
368
|
+
font-size: 14px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
nav a:hover {
|
|
372
|
+
color: #d4d4d4;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
nav .current {
|
|
376
|
+
color: #0e639c;
|
|
377
|
+
font-weight: 600;
|
|
378
|
+
font-size: 14px;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.header-actions {
|
|
382
|
+
display: flex;
|
|
383
|
+
align-items: center;
|
|
384
|
+
gap: 16px;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.share-btn {
|
|
388
|
+
background: #0e639c;
|
|
389
|
+
color: #ffffff;
|
|
390
|
+
border: none;
|
|
391
|
+
padding: 6px 16px;
|
|
392
|
+
border-radius: 3px;
|
|
393
|
+
font-size: 13px;
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
transition: background 0.2s;
|
|
396
|
+
min-width: 80px;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.share-btn:hover {
|
|
400
|
+
background: #1177bb;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.share-btn:active {
|
|
404
|
+
background: #094771;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.status {
|
|
408
|
+
font-size: 12px;
|
|
409
|
+
color: #858585;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
main {
|
|
413
|
+
display: flex;
|
|
414
|
+
flex: 1;
|
|
415
|
+
overflow: hidden;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.pane {
|
|
419
|
+
flex: 1;
|
|
420
|
+
overflow: hidden;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.editor-pane {
|
|
424
|
+
min-width: 200px;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.preview-pane {
|
|
428
|
+
flex: 1;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.divider {
|
|
432
|
+
width: 4px;
|
|
433
|
+
background: #333333;
|
|
434
|
+
cursor: col-resize;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.divider:hover {
|
|
438
|
+
background: #0078d4;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
main.dragging {
|
|
442
|
+
cursor: col-resize;
|
|
443
|
+
user-select: none;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
main.dragging .divider {
|
|
447
|
+
background: #0078d4;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* Drag and drop styles */
|
|
451
|
+
main.drag-over {
|
|
452
|
+
position: relative;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.drop-overlay {
|
|
456
|
+
position: absolute;
|
|
457
|
+
top: 0;
|
|
458
|
+
left: 0;
|
|
459
|
+
right: 0;
|
|
460
|
+
bottom: 0;
|
|
461
|
+
background: rgba(14, 99, 156, 0.9);
|
|
462
|
+
display: flex;
|
|
463
|
+
align-items: center;
|
|
464
|
+
justify-content: center;
|
|
465
|
+
z-index: 100;
|
|
466
|
+
pointer-events: none;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.drop-message {
|
|
470
|
+
color: #ffffff;
|
|
471
|
+
font-size: 18px;
|
|
472
|
+
font-weight: 600;
|
|
473
|
+
padding: 24px 48px;
|
|
474
|
+
border: 3px dashed #ffffff;
|
|
475
|
+
border-radius: 8px;
|
|
476
|
+
text-align: center;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* Log area styles */
|
|
480
|
+
.log-area {
|
|
481
|
+
border-top: 1px solid #454545;
|
|
482
|
+
background: #252526;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.log-toggle {
|
|
486
|
+
width: 100%;
|
|
487
|
+
display: flex;
|
|
488
|
+
align-items: center;
|
|
489
|
+
gap: 8px;
|
|
490
|
+
padding: 6px 12px;
|
|
491
|
+
background: #333333;
|
|
492
|
+
border: none;
|
|
493
|
+
color: #d4d4d4;
|
|
494
|
+
font-size: 12px;
|
|
495
|
+
cursor: pointer;
|
|
496
|
+
text-align: left;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.log-toggle:hover {
|
|
500
|
+
background: #3c3c3c;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.toggle-icon {
|
|
504
|
+
font-size: 10px;
|
|
505
|
+
color: #858585;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.error-badge {
|
|
509
|
+
background: #f14c4c;
|
|
510
|
+
color: #ffffff;
|
|
511
|
+
padding: 1px 6px;
|
|
512
|
+
border-radius: 10px;
|
|
513
|
+
font-size: 11px;
|
|
514
|
+
margin-left: auto;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.log-content {
|
|
518
|
+
max-height: 200px;
|
|
519
|
+
overflow: hidden;
|
|
520
|
+
display: flex;
|
|
521
|
+
flex-direction: column;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.log-actions {
|
|
525
|
+
padding: 4px 12px;
|
|
526
|
+
border-bottom: 1px solid #454545;
|
|
527
|
+
display: flex;
|
|
528
|
+
justify-content: flex-end;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.clear-btn {
|
|
532
|
+
background: transparent;
|
|
533
|
+
border: 1px solid #454545;
|
|
534
|
+
color: #858585;
|
|
535
|
+
padding: 2px 8px;
|
|
536
|
+
border-radius: 3px;
|
|
537
|
+
font-size: 11px;
|
|
538
|
+
cursor: pointer;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.clear-btn:hover {
|
|
542
|
+
background: #3c3c3c;
|
|
543
|
+
color: #d4d4d4;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.log-entries {
|
|
547
|
+
flex: 1;
|
|
548
|
+
overflow-y: auto;
|
|
549
|
+
padding: 8px 12px;
|
|
550
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
551
|
+
font-size: 12px;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.log-entry {
|
|
555
|
+
padding: 2px 0;
|
|
556
|
+
display: flex;
|
|
557
|
+
gap: 8px;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.log-time {
|
|
561
|
+
color: #6a9955;
|
|
562
|
+
flex-shrink: 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.log-level {
|
|
566
|
+
flex-shrink: 0;
|
|
567
|
+
min-width: 60px;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.log-message {
|
|
571
|
+
word-break: break-word;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.log-info .log-level {
|
|
575
|
+
color: #4fc1ff;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.log-warning .log-level {
|
|
579
|
+
color: #cca700;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.log-error .log-level {
|
|
583
|
+
color: #f14c4c;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.log-error .log-message {
|
|
587
|
+
color: #f14c4c;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.log-empty {
|
|
591
|
+
color: #858585;
|
|
592
|
+
font-style: italic;
|
|
593
|
+
text-align: center;
|
|
594
|
+
padding: 16px;
|
|
595
|
+
}
|
|
596
|
+
</style>
|