This commit is contained in:
th committed 2026-04-19 21:40:09 +02:00
1 parent e851d52aef
commit fcefd258eb
6 files changed
+533 -91

No files matched your search

+126 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import {
PhArrowClockwise,
PhArrowCounterClockwise,
@@ -47,6 +47,78 @@ const exportOpen = ref(false)
const testsOpen = ref(false)
const welcomeOpen = ref(false)
const fullscreenOpen = ref(false)
const TIMELINE_HEIGHT_KEY = 'animatr:timelineHeight'
const DEFAULT_TIMELINE_HEIGHT = 220
const MIN_TIMELINE_HEIGHT = 120
const MAX_TIMELINE_HEIGHT_MARGIN = 160
function readStoredTimelineHeight(): number {
try {
const raw = localStorage.getItem(TIMELINE_HEIGHT_KEY)
if (raw === null) return DEFAULT_TIMELINE_HEIGHT
const n = Number.parseInt(raw, 10)
if (Number.isFinite(n) && n >= MIN_TIMELINE_HEIGHT) return n
} catch {
/* ignore */
}
return DEFAULT_TIMELINE_HEIGHT
}
const timelineHeight = ref(readStoredTimelineHeight())
function maxTimelineHeight(): number {
return Math.max(MIN_TIMELINE_HEIGHT, window.innerHeight - MAX_TIMELINE_HEIGHT_MARGIN)
}
const appStyle = computed(() => ({
'--timeline-height': `${timelineHeight.value}px`,
}))
let resizeStartY = 0
let resizeStartHeight = 0
let resizePointerId = -1
const resizing = ref(false)
function onTimelineResizeStart(e: PointerEvent) {
if (e.button !== 0) return
e.preventDefault()
const el = e.currentTarget as HTMLElement
el.setPointerCapture(e.pointerId)
resizePointerId = e.pointerId
resizeStartY = e.clientY
resizeStartHeight = timelineHeight.value
resizing.value = true
document.body.style.userSelect = 'none'
}
function onTimelineResizeMove(e: PointerEvent) {
if (!resizing.value || e.pointerId !== resizePointerId) return
const dy = resizeStartY - e.clientY
const next = Math.max(
MIN_TIMELINE_HEIGHT,
Math.min(maxTimelineHeight(), resizeStartHeight + dy),
)
timelineHeight.value = next
}
function onTimelineResizeEnd(e: PointerEvent) {
if (!resizing.value || e.pointerId !== resizePointerId) return
const el = e.currentTarget as HTMLElement
if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId)
resizePointerId = -1
resizing.value = false
document.body.style.userSelect = ''
try {
localStorage.setItem(TIMELINE_HEIGHT_KEY, String(timelineHeight.value))
} catch {
/* ignore */
}
}
function onWindowResize() {
const max = maxTimelineHeight()
if (timelineHeight.value > max) timelineHeight.value = max
}
const loadError = ref<string | null>(null)
const savedToast = ref<string | null>(null)
const currentProjectId = ref<string | null>(null)
@@ -189,13 +261,17 @@ function onKeyDown(e: KeyboardEvent) {
onMounted(() => {
document.addEventListener('keydown', onKeyDown)
window.addEventListener('resize', onWindowResize)
bootWelcome()
})
onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
onBeforeUnmount(() => {
document.removeEventListener('keydown', onKeyDown)
window.removeEventListener('resize', onWindowResize)
})
</script>
<template>
<div class="app">
<div class="app" :style="appStyle">
<header class="topbar">
<div class="brand-area">
<span class="brand">
@@ -279,7 +355,18 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
</aside>
<main class="stage"><CanvasStage /></main>
<aside class="properties"><PropertiesPanel /></aside>
<footer class="timeline"><Timeline /></footer>
<footer class="timeline">
<div
class="tl-resize"
:class="{ resizing }"
title="Timeline-Höhe ziehen"
@pointerdown="onTimelineResizeStart"
@pointermove="onTimelineResizeMove"
@pointerup="onTimelineResizeEnd"
@pointercancel="onTimelineResizeEnd"
></div>
<Timeline />
</footer>
<DocumentSettings v-model="settingsOpen" />
<ExportDialog v-model="exportOpen" />
<TestOverlay v-model="testsOpen" />
@@ -303,7 +390,7 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
.app {
display: grid;
grid-template-columns: 260px 1fr 300px;
grid-template-rows: 48px 1fr 220px;
grid-template-rows: 48px 1fr var(--timeline-height, 220px);
grid-template-areas:
'top top top'
'side stage props'
@@ -480,6 +567,40 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
background: var(--panel);
border-top: 1px solid var(--border);
overflow: hidden;
position: relative;
}
.tl-resize {
position: absolute;
top: -7px;
left: 0;
right: 0;
height: 14px;
cursor: ns-resize;
z-index: 20;
touch-action: none;
background: transparent;
transition: background 120ms var(--ease);
}
.tl-resize::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 7px;
height: 1px;
background: transparent;
transition: background 120ms var(--ease);
}
.tl-resize:hover::after,
.tl-resize.resizing::after {
background: var(--accent);
}
.tl-resize.resizing {
background: rgba(110, 168, 255, 0.08);
}
.project-name {