0.12.0: document drawer redesign, PWA install, clip/audio context menus + push-cascade
- Document drawer slides out from under the topbar (no tab, no hover peek); opens via project-name button, closes on outside-click or ESC. Layout: inputs row + divider + preset row, presets user-managed via popup and persisted in localStorage. - PWA manifest + minimal service worker so Chrome installs RUWIO as a standalone app — suppresses the native fullscreen exit overlay. - Clips and audio clips get right-click context menus (Duplicate / Delete) via new `duplicateClipInstance` / `duplicateAudioClip` store actions. - Clip moves cascade non-overlap in both directions; `setAudioClipPosition` now pushes backward as well as forward. New `setClipInstancePosition` gives clip instances the same behaviour. - Library drags carry timeline-shaped ghost previews (audio pill, clip pill, button thumb) instead of the library row; clip lane renders a dashed drop-target ghost and rejects window-overlap drops. - Scene / Clipart drawer tabs disappear in fullscreen; the 24 px they occupied is reclaimed for the stage. Play button sits on the exact horizontal center via a 3-column grid in the transport cluster. - Tests: context menu + push-cascade + duplicate action coverage for both clip kinds; DocumentDrawer suite rewritten for the slide-out model. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
fa66ba69bb
commit
354dd8a4f0
25 files changed
+2294
-405
No files matched your search
@@ -1,10 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { onBeforeUnmount, onMounted, watch } from 'vue'
|
||||
import { PhArrowClockwise, PhCaretLeft, PhCaretRight, PhFolderOpen, PhImagesSquare, PhMusicNote } from '@phosphor-icons/vue'
|
||||
import { useClipartStore } from '@/stores/clipart'
|
||||
import { readAudioDuration, readAsDataUrl } from '@/lib/fileLoader'
|
||||
import { clearAudioDragSource, setAudioDragSource } from '@/composables/useAudioDragSource'
|
||||
import {
|
||||
clearAudioDragSource,
|
||||
installAudioClipDragImage,
|
||||
setAudioDragSource,
|
||||
} from '@/composables/useAudioDragSource'
|
||||
import { useFullscreen } from '@/composables/useFullscreen'
|
||||
import { dialogState } from '@/components/ui'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const clipart = useClipartStore()
|
||||
@@ -23,12 +28,31 @@ function onDrawerLeave() {
|
||||
if (isFullscreen.value) clipart.setOpen(false)
|
||||
}
|
||||
|
||||
// ESC closes the drawer when opened via click. Skipped in fullscreen
|
||||
// (hover-leave handles it) and while a dialog is on top.
|
||||
function onDocKeyDown(e: KeyboardEvent) {
|
||||
if (!clipart.open || isFullscreen.value) return
|
||||
if (e.key !== 'Escape') return
|
||||
if (dialogState.value) return
|
||||
e.preventDefault()
|
||||
clipart.setOpen(false)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => clipart.open,
|
||||
(v) => {
|
||||
if (v) document.addEventListener('keydown', onDocKeyDown)
|
||||
else document.removeEventListener('keydown', onDocKeyDown)
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
clipart.initialize()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clipart.disposeItems()
|
||||
document.removeEventListener('keydown', onDocKeyDown)
|
||||
})
|
||||
|
||||
function onItemDragStart(e: DragEvent, index: number) {
|
||||
@@ -44,6 +68,7 @@ function onItemDragStart(e: DragEvent, index: number) {
|
||||
// file decodes, so seed with 0 (ghost renders at minimum width)
|
||||
// and upgrade asynchronously. dragend clears both.
|
||||
setAudioDragSource({ duration: 0, name: item.name })
|
||||
installAudioClipDragImage(e, { name: item.name })
|
||||
const el = e.currentTarget as HTMLElement
|
||||
el.addEventListener('dragend', clearAudioDragSource, { once: true })
|
||||
void (async () => {
|
||||
@@ -55,8 +80,6 @@ function onItemDragStart(e: DragEvent, index: number) {
|
||||
/* leave the placeholder width — still a visible hint */
|
||||
}
|
||||
})()
|
||||
// No image ghost for audio — browsers render the row itself as the
|
||||
// default drag preview, which is what we want.
|
||||
return
|
||||
}
|
||||
// Use the actual image as drag ghost
|
||||
@@ -79,7 +102,11 @@ function onItemDragStart(e: DragEvent, index: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="clipart-drawer" :class="{ open: clipart.open }" @mouseleave="onDrawerLeave">
|
||||
<aside
|
||||
class="clipart-drawer"
|
||||
:class="{ open: clipart.open, fullscreen: isFullscreen }"
|
||||
@mouseleave="onDrawerLeave"
|
||||
>
|
||||
<button
|
||||
class="drawer-tab"
|
||||
v-tooltip="clipart.open ? t('clipart.close') : t('clipart.open')"
|
||||
@@ -192,16 +219,34 @@ function onItemDragStart(e: DragEvent, index: number) {
|
||||
transition: opacity 140ms var(--ease);
|
||||
}
|
||||
|
||||
.clipart-drawer:hover .drawer-tab :deep(svg),
|
||||
.clipart-drawer.open .drawer-tab :deep(svg) {
|
||||
.clipart-drawer:hover:not(.fullscreen) .drawer-tab :deep(svg),
|
||||
.clipart-drawer.open:not(.fullscreen) .drawer-tab :deep(svg) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.clipart-drawer:hover .drawer-tab {
|
||||
.clipart-drawer:hover:not(.fullscreen) .drawer-tab {
|
||||
background: var(--panel-alt);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.clipart-drawer.fullscreen .drawer-tab {
|
||||
cursor: default;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Once the fullscreen hover opens the drawer, the tab column has
|
||||
served its purpose (hover-detect) — reclaim its 24px so the panel
|
||||
uses the full width. The aside itself still catches mouseleave. */
|
||||
.clipart-drawer.fullscreen.open .drawer-tab {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Tab column is gone in fullscreen — reclaim its 24 px so the stage
|
||||
area grows by the same amount instead of leaving a blank strip. */
|
||||
.clipart-drawer.fullscreen.open {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.clipart-drawer.open .drawer-tab {
|
||||
flex: 0 0 24px;
|
||||
width: 24px;
|
||||
|
||||
Reference in new issue
Block a user