Files
RUWIO/src/components/ui/IconButton.vue
T
thandClaude Opus 4.7 1d84c34c15 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>
2026-04-21 23:57:35 +02:00

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>