0.5.1: replace properties checkboxes with icon toggles

All checkboxes and radio buttons in the Properties panel are now
icon-toggle buttons:

- Polygon "Closed path" → PhPolygon button (fill weight when closed)
- Fill/Stroke transparent → PhEye / PhEyeSlash toggle next to the swatch
- Mask use → PhMaskHappy toggle
- Mask mode → segmented control with PhPlus / PhMinus (negative is red)

Shared .icon-toggle style: 26×26, panel background, accent when active,
optional .danger variant. Tests updated to click buttons instead of
checkboxes.

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 12:18:47 +02:00
1 parent ddea7d348f
commit cbc309bc8c
4 files changed
+149 -92

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "animatr", "name": "animatr",
"private": true, "private": true,
"version": "0.5.0", "version": "0.5.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+146 -89
View File
@@ -1,6 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { PhCaretDown, PhCaretRight, PhDiamond, PhTrash, PhX } from '@phosphor-icons/vue' import {
PhCaretDown,
PhCaretRight,
PhDiamond,
PhEye,
PhEyeSlash,
PhMaskHappy,
PhMinus,
PhPolygon,
PhPlus,
PhTrash,
PhX,
} from '@phosphor-icons/vue'
import { useSceneStore } from '@/stores/scene' import { useSceneStore } from '@/stores/scene'
import { sampleLayer } from '@/lib/interpolate' import { sampleLayer } from '@/lib/interpolate'
import type { AnimatableProps } from '@/types/scene' import type { AnimatableProps } from '@/types/scene'
@@ -405,10 +417,18 @@ function removeKeyframeHere() {
<span>{{ t('properties.sections.polygon') }}</span> <span>{{ t('properties.sections.polygon') }}</span>
</button> </button>
<div v-if="sectionOpen.polygon" class="group-body"> <div v-if="sectionOpen.polygon" class="group-body">
<label class="inline-toggle"> <div class="toggle-row">
<input type="checkbox" v-model="polygonClosed" /> <span class="toggle-label">{{ t('properties.polygonClosed') }}</span>
<span>{{ t('properties.polygonClosed') }}</span> <button
</label> type="button"
class="icon-toggle"
:class="{ active: polygonClosed }"
:title="t('properties.polygonClosed')"
@click="polygonClosed = !polygonClosed"
>
<PhPolygon :size="14" :weight="polygonClosed ? 'fill' : 'regular'" />
</button>
</div>
<label> <label>
{{ t('properties.polygonPoints') }} {{ t('properties.polygonPoints') }}
<input type="number" min="3" max="64" v-model.number="polygonPointCount" /> <input type="number" min="3" max="64" v-model.number="polygonPointCount" />
@@ -434,10 +454,15 @@ function removeKeyframeHere() {
@input="fillColor = ($event.target as HTMLInputElement).value" @input="fillColor = ($event.target as HTMLInputElement).value"
/> />
<span class="color-hex">{{ fillTransparent ? '—' : fillColor.toUpperCase() }}</span> <span class="color-hex">{{ fillTransparent ? '—' : fillColor.toUpperCase() }}</span>
<label class="alpha-toggle"> <button
<input type="checkbox" v-model="fillTransparent" /> type="button"
<span>{{ t('properties.transparent') }}</span> class="icon-toggle alpha-toggle-btn"
</label> :class="{ active: fillTransparent }"
:title="t('properties.transparent')"
@click="fillTransparent = !fillTransparent"
>
<component :is="fillTransparent ? PhEyeSlash : PhEye" :size="13" />
</button>
</div> </div>
</label> </label>
<label v-if="canStroke" class="color-label"> <label v-if="canStroke" class="color-label">
@@ -451,10 +476,15 @@ function removeKeyframeHere() {
@input="strokeColor = ($event.target as HTMLInputElement).value" @input="strokeColor = ($event.target as HTMLInputElement).value"
/> />
<span class="color-hex">{{ strokeTransparent ? '—' : strokeColor.toUpperCase() }}</span> <span class="color-hex">{{ strokeTransparent ? '—' : strokeColor.toUpperCase() }}</span>
<label class="alpha-toggle"> <button
<input type="checkbox" v-model="strokeTransparent" /> type="button"
<span>{{ t('properties.transparent') }}</span> class="icon-toggle alpha-toggle-btn"
</label> :class="{ active: strokeTransparent }"
:title="t('properties.transparent')"
@click="strokeTransparent = !strokeTransparent"
>
<component :is="strokeTransparent ? PhEyeSlash : PhEye" :size="13" />
</button>
</div> </div>
</label> </label>
<label v-if="canStroke" class="slider-label"> <label v-if="canStroke" class="slider-label">
@@ -545,31 +575,40 @@ function removeKeyframeHere() {
<span>{{ t('properties.mask') }}</span> <span>{{ t('properties.mask') }}</span>
</button> </button>
<div v-if="sectionOpen.mask" class="group-body"> <div v-if="sectionOpen.mask" class="group-body">
<label class="mask-toggle"> <div class="toggle-row">
<input <span class="toggle-label">{{ t('properties.maskUse') }}</span>
type="checkbox" <button
:checked="!!selected.isMask" type="button"
@change="store.setLayerMask(selected.id, ($event.target as HTMLInputElement).checked)" class="icon-toggle"
/> :class="{ active: !!selected.isMask }"
<span>{{ t('properties.maskUse') }}</span> :title="t('properties.maskUse')"
</label> @click="store.setLayerMask(selected.id, !selected.isMask)"
<div v-if="selected.isMask" class="mask-mode-row"> >
<label class="mask-radio"> <PhMaskHappy :size="14" />
<input </button>
type="radio" </div>
:checked="(selected.maskMode ?? 'positive') === 'positive'" <div v-if="selected.isMask" class="toggle-row">
@change="store.setLayerMaskMode(selected.id, 'positive')" <span class="toggle-label">{{ t('properties.maskMode') }}</span>
/> <div class="segmented">
<span>{{ t('properties.maskPositive') }}</span> <button
</label> type="button"
<label class="mask-radio"> class="icon-toggle"
<input :class="{ active: (selected.maskMode ?? 'positive') === 'positive' }"
type="radio" :title="t('properties.maskPositive')"
:checked="selected.maskMode === 'negative'" @click="store.setLayerMaskMode(selected.id, 'positive')"
@change="store.setLayerMaskMode(selected.id, 'negative')" >
/> <PhPlus :size="13" weight="bold" />
<span>{{ t('properties.maskNegative') }}</span> </button>
</label> <button
type="button"
class="icon-toggle"
:class="{ active: selected.maskMode === 'negative', danger: selected.maskMode === 'negative' }"
:title="t('properties.maskNegative')"
@click="store.setLayerMaskMode(selected.id, 'negative')"
>
<PhMinus :size="13" weight="bold" />
</button>
</div>
</div> </div>
<p v-if="selected.isMask && maskInGroup" class="status-active"> <p v-if="selected.isMask && maskInGroup" class="status-active">
{{ t('properties.maskActive') }} {{ t('properties.maskActive') }}
@@ -681,45 +720,77 @@ input {
padding: 0 2px 10px; padding: 0 2px 10px;
} }
.inline-toggle { .toggle-row {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text);
text-transform: none;
letter-spacing: 0;
cursor: pointer;
flex: 0 0 auto;
}
.inline-toggle input {
width: auto;
margin: 0;
}
.mask-toggle,
.mask-radio {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text);
text-transform: none;
letter-spacing: 0;
cursor: pointer;
flex: 0 0 auto;
}
.mask-mode-row {
display: flex; display: flex;
gap: 16px; align-items: center;
justify-content: space-between;
gap: 10px;
} }
.mask-toggle input, .toggle-label {
.mask-radio input { font-size: 12px;
width: auto; color: var(--text);
margin: 0; text-transform: none;
letter-spacing: 0;
flex: 1 1 auto;
}
.segmented {
display: inline-flex;
gap: 2px;
background: var(--panel-alt);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 2px;
}
.icon-toggle {
width: 26px;
height: 26px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--panel-alt);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-dim);
cursor: pointer;
transition: background 140ms var(--ease), border-color 140ms var(--ease),
color 140ms var(--ease);
flex-shrink: 0;
}
.icon-toggle:hover {
border-color: var(--border-strong);
color: var(--text);
}
.icon-toggle.active {
background: var(--accent-soft);
border-color: var(--accent);
color: var(--accent);
}
.icon-toggle.active.danger {
background: color-mix(in srgb, #ff6b6b 16%, transparent);
border-color: #ff6b6b;
color: #ff6b6b;
}
.segmented .icon-toggle {
border: none;
background: transparent;
}
.segmented .icon-toggle.active {
background: var(--accent-soft);
color: var(--accent);
}
.segmented .icon-toggle.active.danger {
background: color-mix(in srgb, #ff6b6b 16%, transparent);
color: #ff6b6b;
} }
.status-active, .status-active,
@@ -843,22 +914,8 @@ input {
letter-spacing: 0.03em; letter-spacing: 0.03em;
} }
.alpha-toggle { .alpha-toggle-btn {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: auto; margin-left: auto;
font-size: 10px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
flex: 0 0 auto;
}
.alpha-toggle input {
width: auto;
margin: 0;
} }
.color-swatch:disabled { .color-swatch:disabled {
+1 -1
View File
@@ -2399,7 +2399,7 @@ export function registerAllTests(): void {
suite('properties — fill/stroke transparent toggle', () => { suite('properties — fill/stroke transparent toggle', () => {
function findAlphaToggles(host: HTMLElement) { function findAlphaToggles(host: HTMLElement) {
return Array.from( return Array.from(
host.querySelectorAll<HTMLInputElement>('.alpha-toggle input[type="checkbox"]'), host.querySelectorAll<HTMLButtonElement>('button.alpha-toggle-btn'),
) )
} }
+1 -1
View File
@@ -1,2 +1,2 @@
export const APP_VERSION = '0.5.0' export const APP_VERSION = '0.5.1'
export const APP_NAME = 'RUWIO' export const APP_NAME = 'RUWIO'