macro-clips slice 4a: script frame dialog + reusable action editor

- ActionEditor.vue — reusable list editor for Action[]: add/remove/reorder,
  per-type parameter forms (jump to time/frame/marker, set button
  state/disabled, script source). Marker and button dropdowns pull from
  the store's current scene.
- ScriptFrameDialog.vue — modal wrapping ActionEditor plus time and label
  inputs; commits on close, supports deletion.
- Timeline wires the dialog: clicking a script frame opens it for edit,
  clicking the empty script lane creates a new frame at that time and
  opens the editor.
- i18n keys for all action types, parameters, and dialog labels.

Button-authoring UI (definitions + instance placement + properties panel)
is its own slice (4b) — this commit ships script authoring only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-20 20:54:24 +02:00
1 parent 92d58937e3
commit b9d1ca16f7
4 files changed
+635

No files matched your search

+28
View File
@@ -30,6 +30,7 @@ import { useSceneStore } from '@/stores/scene'
import { sampleLayer } from '@/lib/interpolate'
import { snapToCandidates } from '@/lib/timelineSnap'
import { confirmDialog, promptDialog } from '@/components/ui'
import ScriptFrameDialog from './ScriptFrameDialog.vue'
import { t } from '@/i18n'
import { kfSelectionKey, useKfSelection, type KfSelectionKey } from '@/composables/useKfSelection'
import { showLocked } from '@/composables/useShowLocked'
@@ -1478,6 +1479,27 @@ function clipBlockStyle(ci: ClipInstance) {
}
const BUTTON_STATES = ['normal', 'hover', 'pressed', 'disabled'] as const
// Script frame dialog state
const scriptDialogOpen = ref(false)
const scriptDialogFrameId = ref<string | null>(null)
function openScriptFrame(frameId: string) {
scriptDialogFrameId.value = frameId
scriptDialogOpen.value = true
}
function onScriptLaneClick(e: MouseEvent, trackId: string) {
// Only handle clicks on the bare lane (not on an existing frame).
if ((e.target as HTMLElement).closest('.script-frame')) return
const el = e.currentTarget as HTMLElement
const rect = el.getBoundingClientRect()
if (rect.width <= 0) return
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
const time = snapTime(ratio * duration.value)
const frame = store.addScriptFrame(trackId, time)
if (frame) openScriptFrame(frame.id)
}
const BUTTON_CELL_SEC = 0.5 // visual width of each of the 4 state cells
function buttonInstanceStyle(bi: ButtonInstance) {
// Four cells, each with a fixed per-cell visual width. Kept as a fixed
@@ -2311,6 +2333,7 @@ onBeforeUnmount(() => {
<div
class="track-lane script-lane"
:style="{ width: laneWidth + 'px' }"
@click="track.locked ? null : onScriptLaneClick($event, track.scriptTrackId)"
>
<span class="playhead" :style="{ left: pxFromTime(store.playhead) + 'px' }" />
<span
@@ -2319,6 +2342,7 @@ onBeforeUnmount(() => {
class="script-frame"
:style="{ left: pxFromTime(frame.time) + 'px' }"
:title="frame.label ?? t('timeline.scriptFrameTooltip', { n: frame.actions.length })"
@click.stop="openScriptFrame(frame.id)"
>
<span class="script-frame-dot"></span>
<span v-if="frame.actions.length > 1" class="script-frame-count">{{ frame.actions.length }}</span>
@@ -2550,6 +2574,10 @@ onBeforeUnmount(() => {
}}
</button>
</div>
<ScriptFrameDialog
v-model="scriptDialogOpen"
:frame-id="scriptDialogFrameId"
/>
</div>
</template>