This commit is contained in:
2025-12-01 17:42:42 +01:00
commit e9b0e9d856
26 changed files with 3438 additions and 0 deletions

224
src/lib/Palette.svelte Normal file
View File

@@ -0,0 +1,224 @@
<script lang="ts">
import { Square, Type, Image, Music, Video, Globe } from 'lucide-svelte';
import { state } from './state.svelte';
let imageInput: HTMLInputElement;
let soundInput: HTMLInputElement;
let videoInput: HTMLInputElement;
function addTile() {
const id = crypto.randomUUID();
state.addItem({
id,
html: '',
css: '',
x: -state.viewport.x / state.viewport.zoom + 400,
y: -state.viewport.y / state.viewport.zoom + 300,
width: 200,
height: 200,
rotation: 0,
zIndex: state.maxZIndex + 1
});
state.select(id);
}
function addText() {
const id = crypto.randomUUID();
state.addItem({
id,
html: '<p>Text</p>',
css: `p {
color: #fff;
font-size: 24px;
font-family: 'Departure Mono', monospace;
}`,
x: -state.viewport.x / state.viewport.zoom + 400,
y: -state.viewport.y / state.viewport.zoom + 300,
width: 200,
height: 50,
rotation: 0,
zIndex: state.maxZIndex + 1
});
state.select(id);
}
function addEmbed() {
const url = prompt('Enter URL to embed:');
if (!url) return;
const id = crypto.randomUUID();
state.addItem({
id,
html: `<iframe src="${url}" frameborder="0" allowfullscreen></iframe>`,
css: `iframe {
width: 100%;
height: 100%;
border: none;
}`,
x: -state.viewport.x / state.viewport.zoom + 400,
y: -state.viewport.y / state.viewport.zoom + 300,
width: 640,
height: 480,
rotation: 0,
zIndex: state.maxZIndex + 1
});
state.select(id);
}
function handleImageChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
addImageItem(file);
(e.target as HTMLInputElement).value = '';
}
function handleSoundChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
addSoundItem(file);
(e.target as HTMLInputElement).value = '';
}
function handleVideoChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
addVideoItem(file);
(e.target as HTMLInputElement).value = '';
}
function addImageItem(file: File) {
const id = crypto.randomUUID();
const assetId = crypto.randomUUID();
const url = URL.createObjectURL(file);
const x = -state.viewport.x / state.viewport.zoom + 400;
const y = -state.viewport.y / state.viewport.zoom + 300;
const img = new Image();
img.onload = () => {
state.addAsset(assetId, { blob: file, url, filename: file.name });
state.addItem({
id,
assetId,
html: `<img src="${url}" alt="" />`,
css: `img {
width: 100%;
height: 100%;
object-fit: contain;
}`,
x,
y,
width: img.naturalWidth,
height: img.naturalHeight,
rotation: 0,
zIndex: state.maxZIndex + 1
});
state.select(id);
};
img.src = url;
}
function addSoundItem(file: File) {
const id = crypto.randomUUID();
const assetId = crypto.randomUUID();
const url = URL.createObjectURL(file);
const x = -state.viewport.x / state.viewport.zoom + 400;
const y = -state.viewport.y / state.viewport.zoom + 300;
state.addAsset(assetId, { blob: file, url, filename: file.name });
state.addItem({
id,
assetId,
html: `<audio src="${url}" controls></audio>`,
css: `audio {
width: 100%;
}`,
x,
y,
width: 300,
height: 54,
rotation: 0,
zIndex: state.maxZIndex + 1
});
state.select(id);
}
function addVideoItem(file: File) {
const id = crypto.randomUUID();
const assetId = crypto.randomUUID();
const url = URL.createObjectURL(file);
const x = -state.viewport.x / state.viewport.zoom + 400;
const y = -state.viewport.y / state.viewport.zoom + 300;
const video = document.createElement('video');
video.onloadedmetadata = () => {
state.addAsset(assetId, { blob: file, url, filename: file.name });
state.addItem({
id,
assetId,
html: `<video src="${url}" controls></video>`,
css: `video {
width: 100%;
height: 100%;
}`,
x,
y,
width: video.videoWidth || 640,
height: video.videoHeight || 360,
rotation: 0,
zIndex: state.maxZIndex + 1
});
state.select(id);
};
video.src = url;
}
</script>
<div class="palette">
<button onclick={addTile} title="Tile"><Square size={14} /></button>
<button onclick={addText} title="Text"><Type size={14} /></button>
<button onclick={() => imageInput.click()} title="Image"><Image size={14} /></button>
<button onclick={() => soundInput.click()} title="Sound"><Music size={14} /></button>
<button onclick={() => videoInput.click()} title="Video"><Video size={14} /></button>
<button onclick={addEmbed} title="Embed"><Globe size={14} /></button>
<input
bind:this={imageInput}
type="file"
accept="image/*"
onchange={handleImageChange}
style="display:none"
/>
<input
bind:this={soundInput}
type="file"
accept="audio/*"
onchange={handleSoundChange}
style="display:none"
/>
<input
bind:this={videoInput}
type="file"
accept="video/*"
onchange={handleVideoChange}
style="display:none"
/>
</div>
<style>
.palette {
display: flex;
gap: 4px;
}
button {
padding: 4px 8px;
background: var(--surface, #282c34);
color: var(--text-dim, #666);
border: 1px solid var(--border, #333);
cursor: pointer;
}
button:hover {
background: var(--accent, #4a9eff);
color: var(--text, #fff);
}
</style>