0.8.3: custom tooltips, editor-hide, pack script, settings reshuffle
Tooltips - New v-tooltip directive + TooltipHost floating panel replace the native title attribute across 95+ sites (topbar, timeline, sidebar, properties, stage overlays, fullscreen preview, export dialog, clipart drawer, modals). Styled dark card with a configurable label + optional help line for inline documentation; screen readers still get an aria-label when the panel is disabled. - Settings → Interface toggle (`ruwio:tooltipsEnabled`) hides the visual panel for users who know the app. IconButton now routes its title prop through v-tooltip so every existing call site is tooltip-aware without churn. Editor-hide + export filter - New `Layer.editorHidden` flag, eye toggle per layer row, skipped by the Konva stage and the mask-preview canvas path but honoured by exports and fullscreen preview (runtime untouched). - Export dialog gets a "Skip hidden tracks" switch that strips editor-hidden layers from every format (scene JS, runtime, example HTML, MP4, WebM) on opt-in. Disabled when the scene has no hidden layers. Button editor - `+` buttons appear on empty state slots in button-timeline rows and duplicate the nearest keyframe (prev by preference, next otherwise) onto that slot. New `duplicateKeyframeAt` store action carries props, easing, hold, and bezier handles across. Stage / interaction - Plain left-click on the empty content view pans the viewport, but only when the pointer isn't over a Konva shape — transformer handles, vertex dots, and selection boxes that extend past the canvas edge keep their drag. Settings reshuffle - Timeline tab merged into Document (Duration + FPS sit under a new "Timing" sub-heading). New Interface tab gathers three global prefs: Show tooltips, Show locked tracks, Asset thumbnails — driven by shared composables so the existing toolbar toggles stay in sync. Interface-tab CSS defensively resets the scoped label flex-column + uppercase rule that was making the toggle row look like an accident. Project identity + UX polish - Favicon wired up (moved to public/favicon.ico, linked from index.html); scripts/pack.sh packs the minimum Vue project (src, index.html, configs, public) into a tarball with the right version in the filename, and `npm run pack` drives it. - About dialog now credits Torsten Hoeltge. Test infra - Progress bar turns red mid-run on the first failure: runAll gets an onResult callback that TestOverlay uses to push each result into the reactive array incrementally. - mountComponent in the test harness registers v-tooltip on the isolated app instance so IconButton's aria-label contract actually fires under test. Regression tests - duplicateKeyframeAt: copy fidelity (props, easing, hold), collision refusal, unknown-id guards, separated-track targeting. - Shared composables: useTooltipPref and useAssetThumbsPref both return the same ref across calls and persist to localStorage. - Tooltip contract: IconButton ships `aria-label` (not title); live-DOM smoke tests match on aria-label selectors now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a2c8e5998a
commit
1d84c34c15
33 files changed
+1185
-128
No files matched your search
@@ -747,6 +747,7 @@ const stackItems = computed<StackItem[]>(() => {
|
||||
const layers = ba.layers.slice().reverse()
|
||||
for (const layer of layers) {
|
||||
if (!layer.visible) continue
|
||||
if (layer.editorHidden) continue
|
||||
const asset = assetMap.value.get(layer.assetId)
|
||||
if (!asset) continue
|
||||
const props = applyLivePose(sampleLayer(layer, store.playhead), layer.id)
|
||||
@@ -760,6 +761,7 @@ const stackItems = computed<StackItem[]>(() => {
|
||||
const out: StackItem[] = []
|
||||
const layers = store.activeLayers.slice().reverse()
|
||||
for (const layer of layers) {
|
||||
if (layer.editorHidden) continue
|
||||
if (!isLayerActiveAt(layer, store.playhead)) continue
|
||||
const asset = assetMap.value.get(layer.assetId)
|
||||
if (!asset) continue
|
||||
@@ -794,6 +796,10 @@ const stackItems = computed<StackItem[]>(() => {
|
||||
const id = order[i]
|
||||
const layer = layerById.get(id)
|
||||
if (layer) {
|
||||
// Editor-only hide: skip on the Konva stage while the scene
|
||||
// file still carries the layer, so export/fullscreen-preview
|
||||
// render it normally.
|
||||
if (layer.editorHidden) continue
|
||||
if (!isLayerActiveAt(layer, store.playhead)) continue
|
||||
const asset = assetMap.value.get(layer.assetId)
|
||||
if (!asset) continue
|
||||
@@ -2612,13 +2618,33 @@ function isInsideCanvasBounds(e: PointerEvent | MouseEvent): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
function pointerHitsKonvaShape(e: PointerEvent | MouseEvent): boolean {
|
||||
// Transformer handles, vertex dots, and stage shapes can live
|
||||
// outside the canvas rectangle (selection box for a shape near the
|
||||
// edge, a polygon vertex dragged past the border, …). Asking Konva
|
||||
// for an intersection at the pointer lets us tell "user grabbed a
|
||||
// handle" from "user clicked the empty background" even when both
|
||||
// happen outside the canvas bounds.
|
||||
const stage = stageRef.value?.getStage()
|
||||
if (!stage) return false
|
||||
const rect = stage.container().getBoundingClientRect()
|
||||
const hit = stage.getIntersection({
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
})
|
||||
return !!hit
|
||||
}
|
||||
|
||||
function isPanTrigger(e: PointerEvent | MouseEvent) {
|
||||
if (e.button === 1) return true
|
||||
if (e.button === 0 && e.ctrlKey) return true
|
||||
// Plain left-click on the empty content view (outside the canvas
|
||||
// rectangle) pans the viewport too — saves a ctrl-modifier for the
|
||||
// common case of re-centering after zoom.
|
||||
if (e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && !isInsideCanvasBounds(e)) {
|
||||
// common case of re-centering after zoom. Skip panning when the
|
||||
// click is actually on a Konva shape: a transformer handle, a
|
||||
// polygon vertex, or a selection box that extends past the canvas
|
||||
// edge still belongs to its owning shape and must stay draggable.
|
||||
if (e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && !isInsideCanvasBounds(e) && !pointerHitsKonvaShape(e)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -3121,22 +3147,22 @@ onBeforeUnmount(() => {
|
||||
class="hit-tools"
|
||||
@pointerdown.stop
|
||||
>
|
||||
<button class="hit-btn" :title="t('canvas.hit.addRect')" @click="addHitRect">
|
||||
<button class="hit-btn" v-tooltip="t('canvas.hit.addRect')" @click="addHitRect">
|
||||
<PhSquare :size="13" />
|
||||
<span>rect</span>
|
||||
</button>
|
||||
<button class="hit-btn" :title="t('canvas.hit.addCircle')" @click="addHitCircle">
|
||||
<button class="hit-btn" v-tooltip="t('canvas.hit.addCircle')" @click="addHitCircle">
|
||||
<PhCircle :size="13" />
|
||||
<span>circle</span>
|
||||
</button>
|
||||
<button class="hit-btn" :title="t('canvas.hit.addPolygon')" @click="addHitPolygon">
|
||||
<button class="hit-btn" v-tooltip="t('canvas.hit.addPolygon')" @click="addHitPolygon">
|
||||
<PhPolygon :size="13" />
|
||||
<span>polygon</span>
|
||||
</button>
|
||||
<button
|
||||
class="hit-btn danger"
|
||||
:disabled="selectedHitShapeIndex === null"
|
||||
:title="t('canvas.hit.remove')"
|
||||
v-tooltip="t('canvas.hit.remove')"
|
||||
@click="removeSelectedHitShape"
|
||||
>
|
||||
<PhTrash :size="13" />
|
||||
@@ -3144,21 +3170,21 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<div class="zoom-controls" @pointerdown.stop>
|
||||
<button class="zc-btn" :title="t('canvas.zoomOut')" @click="zoomBy(1 / 1.25)">−</button>
|
||||
<button class="zc-label" :title="t('canvas.fitToView')" @click="fitToContainer">
|
||||
<button class="zc-btn" v-tooltip="t('canvas.zoomOut')" @click="zoomBy(1 / 1.25)">−</button>
|
||||
<button class="zc-label" v-tooltip="t('canvas.fitToView')" @click="fitToContainer">
|
||||
{{ zoomPercent }}%
|
||||
</button>
|
||||
<button class="zc-btn" :title="t('canvas.zoomIn')" @click="zoomBy(1.25)">+</button>
|
||||
<button class="zc-btn" v-tooltip="t('canvas.zoomIn')" @click="zoomBy(1.25)">+</button>
|
||||
<div class="zc-sep"></div>
|
||||
<button class="zc-btn wide" :title="t('canvas.zoomOneToOne')" @click="zoomToOne">1:1</button>
|
||||
<button class="zc-btn" :title="t('canvas.fitToView')" @click="fitToContainer">
|
||||
<button class="zc-btn wide" v-tooltip="t('canvas.zoomOneToOne')" @click="zoomToOne">1:1</button>
|
||||
<button class="zc-btn" v-tooltip="t('canvas.fitToView')" @click="fitToContainer">
|
||||
<PhCornersIn :size="13" />
|
||||
</button>
|
||||
<div class="zc-sep"></div>
|
||||
<button
|
||||
class="zc-btn crop-btn"
|
||||
:class="{ active: cropObjects }"
|
||||
:title="cropObjects ? t('canvas.cropOff') : t('canvas.cropOn')"
|
||||
v-tooltip="cropObjects ? t('canvas.cropOff') : t('canvas.cropOn')"
|
||||
@click="cropObjects = !cropObjects"
|
||||
>
|
||||
<PhCrop :size="13" />
|
||||
@@ -3166,7 +3192,7 @@ onBeforeUnmount(() => {
|
||||
<button
|
||||
class="zc-btn"
|
||||
:class="{ active: maskPreview }"
|
||||
:title="maskPreview ? t('canvas.maskPreviewOff') : t('canvas.maskPreviewOn')"
|
||||
v-tooltip="maskPreview ? t('canvas.maskPreviewOff') : t('canvas.maskPreviewOn')"
|
||||
@click="maskPreview = !maskPreview"
|
||||
>
|
||||
<PhMaskHappy v-if="!maskPreview" :size="13" />
|
||||
@@ -3175,7 +3201,7 @@ onBeforeUnmount(() => {
|
||||
<button
|
||||
class="zc-btn stage-preview-btn"
|
||||
:class="{ active: stagePreview }"
|
||||
:title="stagePreview ? t('canvas.stagePreviewOff') : t('canvas.stagePreviewOn')"
|
||||
v-tooltip="stagePreview ? t('canvas.stagePreviewOff') : t('canvas.stagePreviewOn')"
|
||||
@click="stagePreview = !stagePreview"
|
||||
>
|
||||
<PhCursorClick :size="13" />
|
||||
|
||||
Reference in new issue
Block a user