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:
1 parent
ddea7d348f
commit
cbc309bc8c
4 files changed
+149
-92
No files matched your search
+1
-1
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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'
|
||||||
Reference in new issue
Block a user