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>
136 lines
2.8 KiB
Vue
136 lines
2.8 KiB
Vue
<script setup lang="ts">
|
|
import { computed, type Component } from 'vue'
|
|
import type { TooltipContent } from '@/directives/tooltip'
|
|
|
|
type Variant = 'default' | 'ghost' | 'primary' | 'danger'
|
|
type Size = 'xs' | 'sm' | 'md'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
icon?: Component
|
|
iconSize?: number
|
|
iconWeight?: 'regular' | 'bold' | 'fill'
|
|
size?: Size
|
|
variant?: Variant
|
|
active?: boolean
|
|
disabled?: boolean
|
|
rounded?: boolean
|
|
/** Plain tooltip label. Callers pass a simple string. */
|
|
title?: string
|
|
/** Rich tooltip content — takes precedence over `title` when given. */
|
|
tooltip?: string | TooltipContent
|
|
}>(),
|
|
{
|
|
iconWeight: 'regular',
|
|
size: 'sm',
|
|
variant: 'default',
|
|
active: false,
|
|
disabled: false,
|
|
rounded: false,
|
|
},
|
|
)
|
|
|
|
const iconPx: Record<Size, number> = { xs: 12, sm: 14, md: 15 }
|
|
|
|
const tooltipBinding = computed<string | TooltipContent | undefined>(() => {
|
|
if (props.tooltip !== undefined) return props.tooltip
|
|
if (props.title) return props.title
|
|
return undefined
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<button
|
|
type="button"
|
|
class="icon-btn"
|
|
:class="[`size-${size}`, `variant-${variant}`, { active, rounded }]"
|
|
:disabled="disabled"
|
|
v-tooltip="tooltipBinding"
|
|
>
|
|
<slot>
|
|
<component
|
|
v-if="icon"
|
|
:is="icon"
|
|
:size="iconSize ?? iconPx[size]"
|
|
:weight="iconWeight"
|
|
/>
|
|
</slot>
|
|
</button>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.icon-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: var(--text-dim);
|
|
border-radius: var(--radius-sm);
|
|
transition: background 140ms var(--ease), color 140ms var(--ease),
|
|
border-color 140ms var(--ease);
|
|
}
|
|
|
|
.icon-btn.rounded {
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.icon-btn.size-xs {
|
|
width: 22px;
|
|
height: 20px;
|
|
}
|
|
|
|
.icon-btn.size-sm {
|
|
width: 30px;
|
|
height: 26px;
|
|
}
|
|
|
|
.icon-btn.size-md {
|
|
width: 32px;
|
|
height: 30px;
|
|
}
|
|
|
|
.icon-btn:hover:not(:disabled) {
|
|
background: var(--panel-alt);
|
|
color: var(--text);
|
|
}
|
|
|
|
.icon-btn:disabled {
|
|
opacity: 0.35;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.icon-btn.variant-ghost {
|
|
border-color: transparent;
|
|
}
|
|
|
|
.icon-btn.variant-primary {
|
|
background: var(--accent-soft);
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.icon-btn.variant-primary:hover:not(:disabled) {
|
|
background: var(--accent-soft-strong);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.icon-btn.variant-danger:hover:not(:disabled) {
|
|
background: var(--danger-soft);
|
|
color: var(--danger);
|
|
border-color: var(--danger);
|
|
}
|
|
|
|
.icon-btn.active {
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.icon-btn.active:hover:not(:disabled) {
|
|
background: var(--accent-soft-strong);
|
|
color: var(--accent);
|
|
}
|
|
</style>
|