Merge remote-tracking branch 'origin/main' into fix/1898-windows-quit
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -10,6 +10,7 @@ the frozen-backend fallback mirror it for their toolchains.
|
||||
|
||||
**Highlights**
|
||||
- Quitting on Windows is no longer reported as a crash on the next launch (#1898)
|
||||
- A Reduce motion switch in Settings, for calm without changing your whole system (#1857)
|
||||
- A light theme, and System Auto now follows a light-mode OS instead of staying dark (#1973) — thanks @CoDe-ReDz!
|
||||
- Generating from a one-character input now says the input was too short, instead of quoting a convolution error (#1826)
|
||||
- First run asks about text size before the install, not after it (#1849)
|
||||
|
||||
+10
-1
@@ -188,6 +188,7 @@ function App() {
|
||||
|
||||
const locale = useAppStore((s) => s.locale);
|
||||
const font = useAppStore((s) => s.font);
|
||||
const reduceMotion = useAppStore((s) => s.reduceMotion);
|
||||
|
||||
// Hydrate the theme, locale & font so persisted preferences take effect after
|
||||
// zustand persist rehydrates (async from localStorage) and when the user
|
||||
@@ -198,6 +199,14 @@ function App() {
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
// Same reason as the theme above: a persisted preference has to be
|
||||
// re-applied after zustand rehydrates, or the toggle reads as on while
|
||||
// the app animates (#1857).
|
||||
if (reduceMotion) {
|
||||
document.documentElement.setAttribute('data-motion', 'reduce');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-motion');
|
||||
}
|
||||
if (locale) {
|
||||
i18n.changeLanguage(locale);
|
||||
}
|
||||
@@ -206,7 +215,7 @@ function App() {
|
||||
const fontStack = FONT_STACKS[font];
|
||||
if (fontStack) document.documentElement.style.setProperty('--font-sans', fontStack);
|
||||
else document.documentElement.style.removeProperty('--font-sans');
|
||||
}, [locale, theme, font]);
|
||||
}, [locale, theme, font, reduceMotion]);
|
||||
const mode = useAppStore((s) => s.mode);
|
||||
const setMode = useAppStore((s) => s.setMode);
|
||||
// "Define voice" method inside the Voice (studio) workspace — replaces the
|
||||
|
||||
@@ -101,6 +101,8 @@ export default function AppearancePanel() {
|
||||
const setTheme = useAppStore((s) => s.setTheme);
|
||||
const font = useAppStore((s) => s.font);
|
||||
const setFont = useAppStore((s) => s.setFont);
|
||||
const reduceMotion = useAppStore((s) => s.reduceMotion);
|
||||
const setReduceMotion = useAppStore((s) => s.setReduceMotion);
|
||||
const autoPlayPreview = useAppStore((s) => s.autoPlayPreview);
|
||||
const setAutoPlayPreview = useAppStore((s) => s.setAutoPlayPreview);
|
||||
const showHeaderLiveStats = useAppStore((s) => s.showHeaderLiveStats);
|
||||
@@ -289,6 +291,21 @@ export default function AppearancePanel() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title={t('settings.reduce_motion', { defaultValue: 'Reduce motion' })}
|
||||
subtitle={t('settings.reduce_motion_desc', {
|
||||
defaultValue:
|
||||
'Stops background animation and transitions. Your system setting is honoured on its own; this adds to it.',
|
||||
})}
|
||||
control={
|
||||
<SettingsToggle
|
||||
checked={reduceMotion}
|
||||
onChange={setReduceMotion}
|
||||
aria-label={t('settings.reduce_motion', { defaultValue: 'Reduce motion' })}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "متغير البيئة",
|
||||
"theme_auto": "تلقائي حسب النظام",
|
||||
"theme_light": "فاتح"
|
||||
"theme_light": "فاتح",
|
||||
"reduce_motion": "تقليل الحركة",
|
||||
"reduce_motion_desc": "يوقف الرسوم المتحركة في الخلفية والانتقالات. إعداد النظام يظل ساريًا بذاته؛ وهذا يضاف إليه."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Umgebungsvariable",
|
||||
"theme_auto": "System-Standard",
|
||||
"theme_light": "Hell"
|
||||
"theme_light": "Hell",
|
||||
"reduce_motion": "Bewegung reduzieren",
|
||||
"reduce_motion_desc": "Stoppt Hintergrundanimationen und Übergänge. Die Systemeinstellung gilt weiterhin für sich; dies kommt hinzu."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -958,7 +958,9 @@
|
||||
"generate_timeout_shadowed_badge": "Overridden externally",
|
||||
"generate_timeout_shadowed_note": "An environment variable outside VoiceStudio (shell, .env file, or container) is currently setting this — your saved value here is ignored until that variable is removed.",
|
||||
"generate_timeout_shadowed_toast": "Saved, but an environment variable outside VoiceStudio is currently setting this — it will keep being used until that variable is removed.",
|
||||
"hf_token_not_checked": "Not tested"
|
||||
"hf_token_not_checked": "Not tested",
|
||||
"reduce_motion": "Reduce motion",
|
||||
"reduce_motion_desc": "Stops background animation and transitions. Your system setting is honoured on its own; this adds to it."
|
||||
},
|
||||
"about": {
|
||||
"app": "App",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Variable de entorno",
|
||||
"theme_auto": "Automático del sistema",
|
||||
"theme_light": "Claro"
|
||||
"theme_light": "Claro",
|
||||
"reduce_motion": "Reducir movimiento",
|
||||
"reduce_motion_desc": "Detiene las animaciones de fondo y las transiciones. Tu ajuste del sistema se respeta por sí solo; esto se suma."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Variable d’environnement",
|
||||
"theme_auto": "Auto (Système)",
|
||||
"theme_light": "Clair"
|
||||
"theme_light": "Clair",
|
||||
"reduce_motion": "Réduire les animations",
|
||||
"reduce_motion_desc": "Arrête les animations d’arrière-plan et les transitions. Le réglage système reste actif de son côté ; ceci s’y ajoute."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "परिवेश चर",
|
||||
"theme_auto": "सिस्टम डिफ़ॉल्ट",
|
||||
"theme_light": "लाइट"
|
||||
"theme_light": "लाइट",
|
||||
"reduce_motion": "गति कम करें",
|
||||
"reduce_motion_desc": "पृष्ठभूमि एनिमेशन और ट्रांज़िशन रोकता है। आपकी सिस्टम सेटिंग स्वयं लागू रहती है; यह उसमें जुड़ता है।"
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Variabel lingkungan",
|
||||
"theme_auto": "Otomatis Sistem",
|
||||
"theme_light": "Terang"
|
||||
"theme_light": "Terang",
|
||||
"reduce_motion": "Kurangi gerakan",
|
||||
"reduce_motion_desc": "Menghentikan animasi latar dan transisi. Pengaturan sistem Anda tetap berlaku sendiri; ini menambahnya."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Variabile d’ambiente",
|
||||
"theme_auto": "Automatico di sistema",
|
||||
"theme_light": "Chiaro"
|
||||
"theme_light": "Chiaro",
|
||||
"reduce_motion": "Riduci le animazioni",
|
||||
"reduce_motion_desc": "Ferma le animazioni di sfondo e le transizioni. L’impostazione di sistema resta valida da sola; questa si aggiunge."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "環境変数",
|
||||
"theme_auto": "システム自動",
|
||||
"theme_light": "ライト"
|
||||
"theme_light": "ライト",
|
||||
"reduce_motion": "視差効果を減らす",
|
||||
"reduce_motion_desc": "背景アニメーションとトランジションを停止します。システム設定は単独で有効なままで、これはそれに加わります。"
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -577,7 +577,9 @@
|
||||
"generate_timeout_shadowed_toast": "저장되었지만, VoiceStudio 외부의 환경 변수가 현재 이 값을 설정하고 있습니다 — 해당 변수가 제거될 때까지 계속 사용됩니다.",
|
||||
"hf_token_not_checked": "테스트 안 함",
|
||||
"theme_auto": "시스템 자동",
|
||||
"theme_light": "라이트"
|
||||
"theme_light": "라이트",
|
||||
"reduce_motion": "모션 줄이기",
|
||||
"reduce_motion_desc": "배경 애니메이션과 전환을 중지합니다. 시스템 설정은 그대로 적용되며, 이 옵션은 거기에 더해집니다."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Omgevingsvariabele",
|
||||
"theme_auto": "Systeem auto",
|
||||
"theme_light": "Licht"
|
||||
"theme_light": "Licht",
|
||||
"reduce_motion": "Beweging verminderen",
|
||||
"reduce_motion_desc": "Stopt achtergrondanimaties en overgangen. Je systeeminstelling blijft op zichzelf gelden; dit komt daarbij."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Zmienna środowiskowa",
|
||||
"theme_auto": "Automatyczny systemowy",
|
||||
"theme_light": "Jasny"
|
||||
"theme_light": "Jasny",
|
||||
"reduce_motion": "Ogranicz ruch",
|
||||
"reduce_motion_desc": "Zatrzymuje animacje tła i przejścia. Ustawienie systemowe działa samo z siebie; to je uzupełnia."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Variável de ambiente",
|
||||
"theme_auto": "Automático do Sistema",
|
||||
"theme_light": "Claro"
|
||||
"theme_light": "Claro",
|
||||
"reduce_motion": "Reduzir movimento",
|
||||
"reduce_motion_desc": "Para as animações de fundo e as transições. A definição do sistema continua a ser respeitada; esta acrescenta-se."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Переменная окружения",
|
||||
"theme_auto": "Системный",
|
||||
"theme_light": "Светлая"
|
||||
"theme_light": "Светлая",
|
||||
"reduce_motion": "Уменьшить движение",
|
||||
"reduce_motion_desc": "Останавливает фоновые анимации и переходы. Системная настройка действует сама по себе; эта добавляется к ней."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Miljövariabel",
|
||||
"theme_auto": "Systemval",
|
||||
"theme_light": "Ljust"
|
||||
"theme_light": "Ljust",
|
||||
"reduce_motion": "Minska rörelse",
|
||||
"reduce_motion_desc": "Stoppar bakgrundsanimationer och övergångar. Systeminställningen gäller ändå; det här läggs till."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "ตัวแปรสภาพแวดล้อม",
|
||||
"theme_auto": "อัตโนมัติตามระบบ",
|
||||
"theme_light": "สว่าง"
|
||||
"theme_light": "สว่าง",
|
||||
"reduce_motion": "ลดการเคลื่อนไหว",
|
||||
"reduce_motion_desc": "หยุดแอนิเมชันพื้นหลังและทรานซิชัน การตั้งค่าระบบยังมีผลด้วยตัวเอง ส่วนนี้เพิ่มเข้าไป"
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Ortam değişkeni",
|
||||
"theme_auto": "Sistem Otomatik",
|
||||
"theme_light": "Açık"
|
||||
"theme_light": "Açık",
|
||||
"reduce_motion": "Hareketi azalt",
|
||||
"reduce_motion_desc": "Arka plan animasyonlarını ve geçişleri durdurur. Sistem ayarınız kendi başına geçerlidir; bu ona eklenir."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Змінна середовища",
|
||||
"theme_auto": "Системний",
|
||||
"theme_light": "Світла"
|
||||
"theme_light": "Світла",
|
||||
"reduce_motion": "Зменшити рух",
|
||||
"reduce_motion_desc": "Зупиняє фонові анімації та переходи. Системне налаштування діє саме по собі; це доповнює його."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "Biến môi trường",
|
||||
"theme_auto": "Tự động theo hệ thống",
|
||||
"theme_light": "Sáng"
|
||||
"theme_light": "Sáng",
|
||||
"reduce_motion": "Giảm chuyển động",
|
||||
"reduce_motion_desc": "Dừng hoạt ảnh nền và hiệu ứng chuyển tiếp. Cài đặt hệ thống vẫn có hiệu lực riêng; mục này bổ sung thêm."
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -842,7 +842,9 @@
|
||||
"generate_timeout_shadowed_toast": "已保存,但 VoiceStudio 之外的一个环境变量当前正在设置此值——在移除该变量之前,将继续使用该值。",
|
||||
"hf_token_not_checked": "未测试",
|
||||
"theme_auto": "系统自动",
|
||||
"theme_light": "浅色"
|
||||
"theme_light": "浅色",
|
||||
"reduce_motion": "减少动效",
|
||||
"reduce_motion_desc": "停止背景动画和过渡效果。系统设置本身仍然生效,此项在其基础上叠加。"
|
||||
},
|
||||
"about": {
|
||||
"app": "应用",
|
||||
|
||||
@@ -355,7 +355,9 @@
|
||||
"hf_source_cli_label": "HuggingFace CLI",
|
||||
"hf_source_env_label": "環境變數",
|
||||
"theme_auto": "系統自動",
|
||||
"theme_light": "淺色"
|
||||
"theme_light": "淺色",
|
||||
"reduce_motion": "減少動態效果",
|
||||
"reduce_motion_desc": "停止背景動畫與轉場效果。系統設定本身仍然生效,此項在其基礎上疊加。"
|
||||
},
|
||||
"bootstrap": {
|
||||
"title": "VoiceStudio",
|
||||
|
||||
@@ -7704,3 +7704,27 @@ button.dub-stepper__action:focus-visible {
|
||||
overflow-wrap: anywhere;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ── In-app reduced motion (#1857) ─────────────────────────────────────
|
||||
Settings → Appearance → Reduce motion sets `data-motion="reduce"` on the
|
||||
root. This is the same blanket rule the `prefers-reduced-motion` blocks
|
||||
above apply piecemeal, expressed once: it cannot miss an animation the way
|
||||
a per-component list can, and the header status dot is exactly what that
|
||||
list missed.
|
||||
|
||||
Deliberately NOT merged into the media query. The OS preference must keep
|
||||
working on its own, so a user whose system asks for reduced motion still
|
||||
gets it with this toggle off. The two are additive; either one is enough.
|
||||
|
||||
0.01ms rather than 0: a zero duration skips the `animationend` /
|
||||
`transitionend` events some components wait on, which strands them. A
|
||||
duration this short is imperceptible but still fires. */
|
||||
[data-motion='reduce'],
|
||||
[data-motion='reduce'] *,
|
||||
[data-motion='reduce'] *::before,
|
||||
[data-motion='reduce'] *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
|
||||
@@ -269,6 +269,20 @@ export interface PrefsSlice {
|
||||
langPromptSeen: boolean;
|
||||
setLangPromptSeen: (seen: boolean) => void;
|
||||
|
||||
/**
|
||||
* Force reduced motion regardless of the OS setting.
|
||||
*
|
||||
* The CSS already honours `prefers-reduced-motion` in a dozen places, but
|
||||
* that is the OS switch and nothing else. Someone who wants a calm app
|
||||
* without turning motion off system-wide had no way to ask for it, and
|
||||
* someone whose OS setting is not respected by their environment had no
|
||||
* recourse at all (#1857). This is additive: the media query still applies
|
||||
* on its own, so turning this off never re-enables motion for a user whose
|
||||
* OS asked for less.
|
||||
*/
|
||||
reduceMotion: boolean;
|
||||
setReduceMotion: (on: boolean) => void;
|
||||
|
||||
theme: ThemeId;
|
||||
setTheme: (id: ThemeId) => void;
|
||||
|
||||
@@ -419,6 +433,16 @@ export const createPrefsSlice: StateCreator<PrefsSlice, [], [], PrefsSlice> = (s
|
||||
langPromptSeen: false,
|
||||
setLangPromptSeen: (seen) => set({ langPromptSeen: seen }),
|
||||
|
||||
reduceMotion: false,
|
||||
setReduceMotion: (on) => {
|
||||
set({ reduceMotion: on });
|
||||
if (on) {
|
||||
document.documentElement.setAttribute('data-motion', 'reduce');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-motion');
|
||||
}
|
||||
},
|
||||
|
||||
theme: 'gruvbox',
|
||||
setTheme: (id) => {
|
||||
set({ theme: id });
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* #1857 Part A — an in-app motion control.
|
||||
*
|
||||
* The CSS honours `prefers-reduced-motion` in about a dozen separate blocks,
|
||||
* but that is the OS switch and nothing else. Someone who wants a calm app
|
||||
* without turning motion off system-wide had no way to ask, and someone whose
|
||||
* OS setting is not respected by their environment had no recourse at all.
|
||||
*
|
||||
* Two properties matter and both are pinned here: the toggle drives a single
|
||||
* root attribute (so it cannot miss an animation the way a per-component list
|
||||
* can — the header status dot is exactly what that list missed, Part B), and
|
||||
* it is ADDITIVE, so turning it off never disables the OS media query.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { useAppStore } from '../store';
|
||||
|
||||
const css = readFileSync(resolve(process.cwd(), 'src/index.css'), 'utf8');
|
||||
|
||||
describe('reduce-motion toggle', () => {
|
||||
beforeEach(() => {
|
||||
useAppStore.getState().setReduceMotion(false);
|
||||
});
|
||||
|
||||
it('sets and clears a single root attribute', () => {
|
||||
useAppStore.getState().setReduceMotion(true);
|
||||
expect(document.documentElement.getAttribute('data-motion')).toBe('reduce');
|
||||
expect(useAppStore.getState().reduceMotion).toBe(true);
|
||||
|
||||
useAppStore.getState().setReduceMotion(false);
|
||||
expect(document.documentElement.getAttribute('data-motion')).toBeNull();
|
||||
expect(useAppStore.getState().reduceMotion).toBe(false);
|
||||
});
|
||||
|
||||
it('is off by default', () => {
|
||||
expect(useAppStore.getState().reduceMotion).toBe(false);
|
||||
});
|
||||
|
||||
it('has a blanket rule that covers descendants and pseudo-elements', () => {
|
||||
// A per-component list is what let the header dot keep pulsing. One rule
|
||||
// over the whole tree cannot have that gap.
|
||||
expect(css).toMatch(/\[data-motion='reduce'\] \*,/);
|
||||
expect(css).toMatch(/\[data-motion='reduce'\] \*::before,/);
|
||||
expect(css).toMatch(/\[data-motion='reduce'\] \*::after/);
|
||||
});
|
||||
|
||||
it('does not fold the OS media query into the toggle', () => {
|
||||
// The two must stay independent: the media query has to keep working for
|
||||
// a user who never opens Settings.
|
||||
const block = css.slice(css.indexOf("[data-motion='reduce'],"));
|
||||
expect(block).not.toContain('prefers-reduced-motion');
|
||||
expect(css).toContain('prefers-reduced-motion');
|
||||
});
|
||||
|
||||
it('uses a near-zero duration rather than none, so end events still fire', () => {
|
||||
// A zero duration skips animationend/transitionend, stranding anything
|
||||
// that waits on them.
|
||||
const block = css.slice(css.indexOf("[data-motion='reduce'],"));
|
||||
expect(block).toContain('animation-duration: 0.01ms !important');
|
||||
expect(block).not.toMatch(/animation:\s*none/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user