0.5.5: inline lock toggle, proper lock indentation

- Track lock becomes a small inline icon toggle sitting next to the
  track label, replacing the static badge and removing the context
  menu's lock/unlock entry.
- Locked tracks are fully inert: the lane and name have pointer-events
  disabled, only the lock button itself stays clickable. The pile of
  hover-neutralizing CSS rules is gone.
- Lock button is icon-only (no background/border box) and swaps to the
  opposite icon on hover (filled unlock when hovering a locked track,
  so the click affordance is obvious).
- Locked tracks in groups keep their indentation — the drag-handle
  switches from display:none to visibility:hidden so the reserved
  space still aligns with unlocked sibling layers.

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 14:37:40 +02:00
1 parent 6786ad739a
commit 63042b284d
3 files changed
+134 -78

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "animatr",
"private": true,
"version": "0.5.4",
"version": "0.5.5",
"type": "module",
"scripts": {
"dev": "vite",
+132 -76
View File
@@ -1338,12 +1338,6 @@ function onClipTrackContextMenu(e: MouseEvent, clipTrackId: string) {
}
}
function toggleLocked() {
if (!trackMenu.value) return
store.setTrackLocked(trackMenu.value.trackId, !trackMenu.value.locked)
trackMenu.value = null
}
function applyTrackColor(color: string | null) {
if (!trackMenu.value) return
store.setTrackColor(trackMenu.value.trackId, color)
@@ -1613,18 +1607,21 @@ onBeforeUnmount(() => {
parent: track.isParent,
sub: !track.isParent,
}"
:draggable="track.isParent"
@click="store.selectLayer(track.layerId)"
@contextmenu="track.isParent ? onTrackContextMenu($event, track.layerId) : null"
@dragstart="track.isParent ? onTrackDragStart($event, track.layerId) : null"
:draggable="track.isParent && !track.layer.locked"
@click="!track.layer.locked ? store.selectLayer(track.layerId) : null"
@contextmenu="
track.isParent && !track.layer.locked
? onTrackContextMenu($event, track.layerId)
: null
"
@dragstart="
track.isParent && !track.layer.locked
? onTrackDragStart($event, track.layerId)
: null
"
@dragend="onTrackDragEnd"
>
<PhDotsSixVertical v-if="track.isParent" :size="12" class="drag-handle" />
<PhLock
v-if="track.isParent && track.layer.locked"
:size="11"
class="lock-badge"
/>
<PhArrowsSplit
v-if="track.isParent && track.layer.separated"
:size="11"
@@ -1637,7 +1634,27 @@ onBeforeUnmount(() => {
:class="{ 'mask-negative': track.layer.maskMode === 'negative' }"
/>
<span v-if="!track.isParent" class="sub-indent">↳</span>
{{ track.label }}
<span class="track-label">{{ track.label }}</span>
<button
v-if="track.isParent"
class="lock-btn"
:class="{ locked: track.layer.locked }"
:title="track.layer.locked ? t('timeline.unlock') : t('timeline.lock')"
@click.stop="store.setTrackLocked(track.layerId, !track.layer.locked)"
@mousedown.stop
@dragstart.prevent.stop
>
<span class="lock-icon rest">
<component :is="track.layer.locked ? PhLock : PhLockOpen" :size="12" />
</span>
<span class="lock-icon alt">
<component
:is="track.layer.locked ? PhLockOpen : PhLock"
:size="12"
:weight="track.layer.locked ? 'fill' : 'regular'"
/>
</span>
</button>
</span>
<div
class="track-lane"
@@ -1704,19 +1721,49 @@ onBeforeUnmount(() => {
>
<span
class="track-name clip-track-name"
draggable="true"
@contextmenu="onClipTrackContextMenu($event, track.clipTrackId)"
@dragstart="onTrackDragStart($event, track.clipTrackId)"
:draggable="!clipTrackById.get(track.clipTrackId)?.locked"
@contextmenu="
!clipTrackById.get(track.clipTrackId)?.locked
? onClipTrackContextMenu($event, track.clipTrackId)
: null
"
@dragstart="
!clipTrackById.get(track.clipTrackId)?.locked
? onTrackDragStart($event, track.clipTrackId)
: null
"
@dragend="onTrackDragEnd"
>
<PhDotsSixVertical :size="12" class="drag-handle" />
<PhLock
v-if="clipTrackById.get(track.clipTrackId)?.locked"
:size="11"
class="lock-badge"
/>
<PhFilmStrip :size="11" class="name-badge" />
{{ track.name }}
<span class="track-label">{{ track.name }}</span>
<button
class="lock-btn"
:class="{ locked: !!clipTrackById.get(track.clipTrackId)?.locked }"
:title="clipTrackById.get(track.clipTrackId)?.locked ? t('timeline.unlock') : t('timeline.lock')"
@click.stop="
store.setTrackLocked(
track.clipTrackId,
!clipTrackById.get(track.clipTrackId)?.locked,
)
"
@mousedown.stop
@dragstart.prevent.stop
>
<span class="lock-icon rest">
<component
:is="clipTrackById.get(track.clipTrackId)?.locked ? PhLock : PhLockOpen"
:size="12"
/>
</span>
<span class="lock-icon alt">
<component
:is="clipTrackById.get(track.clipTrackId)?.locked ? PhLockOpen : PhLock"
:size="12"
:weight="clipTrackById.get(track.clipTrackId)?.locked ? 'fill' : 'regular'"
/>
</span>
</button>
</span>
<div
class="track-lane clip-lane"
@@ -1949,15 +1996,8 @@ onBeforeUnmount(() => {
</button>
</template>
<template v-else>
<div class="kf-menu-divider"></div>
<button class="kf-menu-item" @click="toggleLocked">
<span class="check">
<PhCheck v-if="trackMenu.locked" :size="12" weight="bold" />
</span>
<component :is="trackMenu.locked ? PhLockOpen : PhLock" :size="12" class="kf-menu-icon" />
{{ trackMenu.locked ? t('timeline.unlock') : t('timeline.lock') }}
</button>
<template v-if="trackMenu.kind === 'layer'">
<div class="kf-menu-divider"></div>
<button
class="kf-menu-item"
:disabled="trackMenu.locked"
@@ -2292,77 +2332,93 @@ onBeforeUnmount(() => {
transparent 4px,
transparent 8px
);
cursor: not-allowed;
cursor: default;
/* Locked lane is fully inert: no hover, no click, no drag, nothing. */
pointer-events: none;
}
.track.locked .kf,
.track.locked .kf-seg,
.track.locked .clip-block {
opacity: 0.55;
cursor: not-allowed;
}
.track.locked .drag-handle {
display: none;
/* Keep the layout space so labels align with unlocked siblings — just hide
the grip visually. */
visibility: hidden;
}
.lock-badge {
/* Locked track-name is inert except for the lock toggle itself. */
.track.locked .track-name {
cursor: default;
pointer-events: none;
/* Keep the dimmed look but don't react to hover */
color: var(--text-dim);
margin-right: 4px;
}
.track.locked .track-name .lock-btn {
pointer-events: auto;
}
.lock-btn {
margin-left: auto;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
background: transparent;
border: none;
color: var(--text-dim);
cursor: pointer;
flex-shrink: 0;
opacity: 0.85;
opacity: 0;
transition: opacity 140ms var(--ease), color 140ms var(--ease);
}
/* Neutralize hover states on locked tracks */
.track.locked .track-name:hover {
background: var(--panel);
color: var(--text-dim);
.track-name:hover .lock-btn {
opacity: 0.7;
}
.track.locked .track-name.selected:hover {
background:
linear-gradient(var(--accent-soft), var(--accent-soft)),
var(--panel);
.lock-btn:hover {
color: var(--text);
opacity: 1;
}
.lock-btn.locked {
opacity: 1;
color: var(--text);
}
.lock-btn.locked:hover {
color: var(--accent);
}
.track.locked.sub-track .track-name:hover {
background: var(--panel-alt);
.lock-icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
}
.track.locked.inactive:hover .track-lane {
opacity: 0.4;
.lock-icon.alt {
display: none;
}
.track.locked .clip-block:hover {
background: color-mix(in srgb, var(--track-color, var(--accent)) 14%, transparent);
transform: none;
.lock-btn:hover .lock-icon.rest {
display: none;
}
.track.locked .clip-block:hover .clip-handle {
opacity: 0;
.lock-btn:hover .lock-icon.alt {
display: inline-flex;
}
.track.locked .clip-block.trimmed-start:hover .clip-handle.start,
.track.locked .clip-block.trimmed-end:hover .clip-handle.end {
opacity: 0.55;
}
.track.locked .clip-handle:hover,
.track.locked .clip-handle:active {
background: transparent !important;
opacity: 0 !important;
}
.track.locked .kf:hover {
background: var(--track-color, var(--accent));
transform: rotate(45deg);
}
.track.locked .kf.hold:hover {
background: var(--panel);
border-color: var(--track-color, var(--accent));
transform: none;
.track-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
flex: 1 1 auto;
}
.track-name {
+1 -1
View File
@@ -1,2 +1,2 @@
export const APP_VERSION = '0.5.4'
export const APP_VERSION = '0.5.5'
export const APP_NAME = 'RUWIO'