fixes
This commit is contained in:
148
src/App.svelte
148
src/App.svelte
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user