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", "name": "animatr",
"private": true, "private": true,
"version": "0.5.4", "version": "0.5.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "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) { function applyTrackColor(color: string | null) {
if (!trackMenu.value) return if (!trackMenu.value) return
store.setTrackColor(trackMenu.value.trackId, color) store.setTrackColor(trackMenu.value.trackId, color)
@@ -1613,18 +1607,21 @@ onBeforeUnmount(() => {
parent: track.isParent, parent: track.isParent,
sub: !track.isParent, sub: !track.isParent,
}" }"
:draggable="track.isParent" :draggable="track.isParent && !track.layer.locked"
@click="store.selectLayer(track.layerId)" @click="!track.layer.locked ? store.selectLayer(track.layerId) : null"
@contextmenu="track.isParent ? onTrackContextMenu($event, track.layerId) : null" @contextmenu="
@dragstart="track.isParent ? onTrackDragStart($event, track.layerId) : null" track.isParent && !track.layer.locked
? onTrackContextMenu($event, track.layerId)
: null
"
@dragstart="
track.isParent && !track.layer.locked
? onTrackDragStart($event, track.layerId)
: null
"
@dragend="onTrackDragEnd" @dragend="onTrackDragEnd"
> >
<PhDotsSixVertical v-if="track.isParent" :size="12" class="drag-handle" /> <PhDotsSixVertical v-if="track.isParent" :size="12" class="drag-handle" />
<PhLock
v-if="track.isParent && track.layer.locked"
:size="11"
class="lock-badge"
/>
<PhArrowsSplit <PhArrowsSplit
v-if="track.isParent && track.layer.separated" v-if="track.isParent && track.layer.separated"
:size="11" :size="11"
@@ -1637,7 +1634,27 @@ onBeforeUnmount(() => {
:class="{ 'mask-negative': track.layer.maskMode === 'negative' }" :class="{ 'mask-negative': track.layer.maskMode === 'negative' }"
/> />
<span v-if="!track.isParent" class="sub-indent">↳</span> <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> </span>
<div <div
class="track-lane" class="track-lane"
@@ -1704,19 +1721,49 @@ onBeforeUnmount(() => {
> >
<span <span
class="track-name clip-track-name" class="track-name clip-track-name"
draggable="true" :draggable="!clipTrackById.get(track.clipTrackId)?.locked"
@contextmenu="onClipTrackContextMenu($event, track.clipTrackId)" @contextmenu="
@dragstart="onTrackDragStart($event, track.clipTrackId)" !clipTrackById.get(track.clipTrackId)?.locked
? onClipTrackContextMenu($event, track.clipTrackId)
: null
"
@dragstart="
!clipTrackById.get(track.clipTrackId)?.locked
? onTrackDragStart($event, track.clipTrackId)
: null
"
@dragend="onTrackDragEnd" @dragend="onTrackDragEnd"
> >
<PhDotsSixVertical :size="12" class="drag-handle" /> <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" /> <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> </span>
<div <div
class="track-lane clip-lane" class="track-lane clip-lane"
@@ -1949,15 +1996,8 @@ onBeforeUnmount(() => {
</button> </button>
</template> </template>
<template v-else> <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'"> <template v-if="trackMenu.kind === 'layer'">
<div class="kf-menu-divider"></div>
<button <button
class="kf-menu-item" class="kf-menu-item"
:disabled="trackMenu.locked" :disabled="trackMenu.locked"
@@ -2292,77 +2332,93 @@ onBeforeUnmount(() => {
transparent 4px, transparent 4px,
transparent 8px 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,
.track.locked .kf-seg, .track.locked .kf-seg,
.track.locked .clip-block { .track.locked .clip-block {
opacity: 0.55; opacity: 0.55;
cursor: not-allowed;
} }
.track.locked .drag-handle { .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); 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; flex-shrink: 0;
opacity: 0.85; opacity: 0;
transition: opacity 140ms var(--ease), color 140ms var(--ease);
} }
/* Neutralize hover states on locked tracks */ .track-name:hover .lock-btn {
.track.locked .track-name:hover { opacity: 0.7;
background: var(--panel);
color: var(--text-dim);
} }
.track.locked .track-name.selected:hover { .lock-btn:hover {
background: color: var(--text);
linear-gradient(var(--accent-soft), var(--accent-soft)), opacity: 1;
var(--panel); }
.lock-btn.locked {
opacity: 1;
color: var(--text);
}
.lock-btn.locked:hover {
color: var(--accent); color: var(--accent);
} }
.track.locked.sub-track .track-name:hover { .lock-icon {
background: var(--panel-alt); display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
} }
.track.locked.inactive:hover .track-lane { .lock-icon.alt {
opacity: 0.4; display: none;
} }
.track.locked .clip-block:hover { .lock-btn:hover .lock-icon.rest {
background: color-mix(in srgb, var(--track-color, var(--accent)) 14%, transparent); display: none;
transform: none;
} }
.track.locked .clip-block:hover .clip-handle { .lock-btn:hover .lock-icon.alt {
opacity: 0; display: inline-flex;
} }
.track.locked .clip-block.trimmed-start:hover .clip-handle.start, .track-label {
.track.locked .clip-block.trimmed-end:hover .clip-handle.end { overflow: hidden;
opacity: 0.55; text-overflow: ellipsis;
} white-space: nowrap;
min-width: 0;
.track.locked .clip-handle:hover, flex: 1 1 auto;
.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-name { .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' export const APP_NAME = 'RUWIO'