This commit is contained in:
2026-01-22 10:51:41 +01:00
parent aa550c96b7
commit d6b9a3dbff
6 changed files with 167 additions and 9 deletions

View File

@@ -4,6 +4,7 @@
import Toolbar from './lib/Toolbar.svelte'; import Toolbar from './lib/Toolbar.svelte';
import Editor from './lib/Editor.svelte'; import Editor from './lib/Editor.svelte';
import { state as appState } from './lib/state.svelte'; import { state as appState } from './lib/state.svelte';
import { exportBoard } from './lib/io.svelte';
let editingItem = $derived(appState.editingId ? appState.getItem(appState.editingId) : null); let editingItem = $derived(appState.editingId ? appState.getItem(appState.editingId) : null);
let showEditor = $derived(editingItem || appState.editingGlobal); let showEditor = $derived(editingItem || appState.editingGlobal);
@@ -11,6 +12,9 @@
let editorWidth = $state(320); let editorWidth = $state(320);
let isResizing = $state(false); let isResizing = $state(false);
let interfaceHidden = $state(false); let interfaceHidden = $state(false);
let exportModalOpen = $state(false);
let exportFilename = $state('board');
let exportInput: HTMLInputElement;
$effect(() => { $effect(() => {
if (appState.editingId || appState.editingGlobal) { if (appState.editingId || appState.editingGlobal) {
@@ -18,6 +22,13 @@
} }
}); });
$effect(() => {
if (exportModalOpen && exportInput) {
exportInput.focus();
exportInput.select();
}
});
function handleResizeStart(e: MouseEvent) { function handleResizeStart(e: MouseEvent) {
e.preventDefault(); e.preventDefault();
isResizing = true; isResizing = true;
@@ -32,6 +43,28 @@
function handleResizeEnd() { function handleResizeEnd() {
isResizing = false; isResizing = false;
} }
function openExportModal() {
exportFilename = 'board';
exportModalOpen = true;
}
async function confirmExport() {
const result = await exportBoard(exportFilename.trim() || 'board');
if (!result.success) {
alert(result.error || 'Export failed');
}
exportModalOpen = false;
}
function cancelExport() {
exportModalOpen = false;
}
function handleExportKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') cancelExport();
if (e.key === 'Enter') confirmExport();
}
</script> </script>
<svelte:head> <svelte:head>
@@ -43,7 +76,7 @@
<div class="app" class:resizing={isResizing}> <div class="app" class:resizing={isResizing}>
{#if !interfaceHidden} {#if !interfaceHidden}
<Toolbar onHide={() => { interfaceHidden = true; appState.setLocked(true); }} /> <Toolbar onHide={() => { interfaceHidden = true; appState.setLocked(true); }} onExport={openExportModal} />
{/if} {/if}
<div class="workspace"> <div class="workspace">
<div class="canvas-container"> <div class="canvas-container">
@@ -82,6 +115,32 @@
{/each} {/each}
</div> </div>
{/if} {/if}
{#if exportModalOpen}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="modal-overlay" onclick={cancelExport} onkeydown={handleExportKeydown}>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="modal" onclick={(e) => e.stopPropagation()}>
<div class="modal-title">Export Board</div>
<div class="modal-field">
<label for="export-filename">Filename</label>
<div class="filename-input">
<input
id="export-filename"
type="text"
bind:value={exportFilename}
bind:this={exportInput}
onkeydown={handleExportKeydown}
/>
<span class="suffix">.bub</span>
</div>
</div>
<div class="modal-actions">
<button onclick={cancelExport}>Cancel</button>
<button class="primary" onclick={confirmExport}>Export</button>
</div>
</div>
</div>
{/if}
</div> </div>
<style> <style>
@@ -138,6 +197,7 @@
background: var(--surface, #282c34); background: var(--surface, #282c34);
color: var(--text-dim, #666); color: var(--text-dim, #666);
border: 1px solid var(--border, #333); border: 1px solid var(--border, #333);
font-family: inherit;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -161,4 +221,90 @@
background: var(--accent, #4a9eff); background: var(--accent, #4a9eff);
color: var(--text, #fff); color: var(--text, #fff);
} }
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: var(--surface, #282c34);
border: 1px solid var(--border, #333);
padding: 16px;
min-width: 280px;
}
.modal-title {
font-size: 14px;
font-weight: bold;
color: var(--text, #fff);
margin-bottom: 16px;
}
.modal-field {
margin-bottom: 16px;
}
.modal-field label {
display: block;
font-size: 12px;
color: var(--text-dim, #666);
margin-bottom: 4px;
}
.filename-input {
display: flex;
align-items: center;
background: var(--surface, #282c34);
border: 1px solid var(--border, #333);
}
.filename-input input {
flex: 1;
background: transparent;
border: none;
padding: 8px;
color: var(--text, #fff);
font-family: inherit;
font-size: 14px;
outline: none;
}
.filename-input .suffix {
padding: 8px;
color: var(--text-dim, #666);
font-size: 14px;
border-left: 1px solid var(--border, #333);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.modal-actions button {
padding: 6px 12px;
background: var(--surface, #282c34);
color: var(--text-dim, #666);
border: 1px solid var(--border, #333);
font-family: inherit;
cursor: pointer;
font-size: 12px;
}
.modal-actions button:hover {
background: var(--accent, #4a9eff);
color: var(--text, #fff);
}
.modal-actions button.primary {
background: var(--accent, #4a9eff);
color: var(--text, #fff);
}
</style> </style>

View File

@@ -14,6 +14,13 @@
box-sizing: border-box; box-sizing: border-box;
} }
button,
input,
select,
textarea {
font-family: inherit;
}
:root { :root {
font-family: 'Departure Mono', monospace; font-family: 'Departure Mono', monospace;
color: var(--text, #fff); color: var(--text, #fff);

View File

@@ -221,6 +221,7 @@
background: transparent; background: transparent;
border: none; border: none;
color: var(--text-dim, #666); color: var(--text-dim, #666);
font-family: inherit;
cursor: pointer; cursor: pointer;
} }
@@ -237,6 +238,7 @@
background: transparent; background: transparent;
border: none; border: none;
color: var(--text-dim, #666); color: var(--text-dim, #666);
font-family: inherit;
cursor: pointer; cursor: pointer;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;
@@ -251,6 +253,7 @@
background: transparent; background: transparent;
border: none; border: none;
color: var(--text-dim, #666); color: var(--text-dim, #666);
font-family: inherit;
cursor: pointer; cursor: pointer;
} }

View File

@@ -236,6 +236,7 @@
background: var(--surface, #282c34); background: var(--surface, #282c34);
color: var(--text-dim, #666); color: var(--text-dim, #666);
border: 1px solid var(--border, #333); border: 1px solid var(--border, #333);
font-family: inherit;
cursor: pointer; cursor: pointer;
} }

View File

@@ -1,18 +1,17 @@
<script lang="ts"> <script lang="ts">
import { Upload, Download, Paintbrush, Trash2, EyeOff, ZoomIn, Combine, Lock, Unlock, Grid3x3 } from 'lucide-svelte'; import { Upload, Download, Paintbrush, Trash2, EyeOff, ZoomIn, Combine, Lock, Unlock, Grid3x3 } from 'lucide-svelte';
import { exportBoard, importBoard, mergeBoard } from './io.svelte'; import { importBoard, mergeBoard } from './io.svelte';
import { state } from './state.svelte'; import { state } from './state.svelte';
import Palette from './Palette.svelte'; import Palette from './Palette.svelte';
let { onHide }: { onHide?: () => void } = $props(); let { onHide, onExport }: { onHide?: () => void; onExport?: () => void } = $props();
let fileInput: HTMLInputElement; let fileInput: HTMLInputElement;
let mergeFileInput: HTMLInputElement; let mergeFileInput: HTMLInputElement;
async function handleExport() { function handleExport() {
const result = await exportBoard(); if (onExport) {
if (!result.success) { onExport();
alert(result.error || 'Export failed');
} }
} }
@@ -186,6 +185,7 @@
width: 24px; width: 24px;
height: 24px; height: 24px;
padding: 0; padding: 0;
font-family: inherit;
font-size: 11px; font-size: 11px;
font-weight: bold; font-weight: bold;
} }
@@ -200,6 +200,7 @@
background: var(--surface, #282c34); background: var(--surface, #282c34);
color: var(--text-dim, #666); color: var(--text-dim, #666);
border: 1px solid var(--border, #333); border: 1px solid var(--border, #333);
font-family: inherit;
cursor: pointer; cursor: pointer;
} }

View File

@@ -2,7 +2,7 @@ import JSZip from 'jszip';
import type { Manifest, AssetStore, Item } from './types'; import type { Manifest, AssetStore, Item } from './types';
import { state } from './state.svelte'; import { state } from './state.svelte';
export async function exportBoard(): Promise<{ export async function exportBoard(filename = 'board'): Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
}> { }> {
@@ -51,7 +51,7 @@ export async function exportBoard(): Promise<{
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = 'board.bub'; a.download = `${filename}.bub`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
return { success: true }; return { success: true };