Merge remote-tracking branch 'origin/main' into fix/1898-windows-quit

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Palash Debnath
2026-09-09 21:45:17 -07:00
27 changed files with 203 additions and 22 deletions
+1
View File
@@ -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
View File
@@ -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>
);
}
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -355,7 +355,9 @@
"hf_source_cli_label": "HuggingFace CLI",
"hf_source_env_label": "Variable denvironnement",
"theme_auto": "Auto (Système)",
"theme_light": "Clair"
"theme_light": "Clair",
"reduce_motion": "Réduire les animations",
"reduce_motion_desc": "Arrête les animations darrière-plan et les transitions. Le réglage système reste actif de son côté ; ceci sy ajoute."
},
"bootstrap": {
"title": "VoiceStudio",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -355,7 +355,9 @@
"hf_source_cli_label": "HuggingFace CLI",
"hf_source_env_label": "Variabile dambiente",
"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. Limpostazione di sistema resta valida da sola; questa si aggiunge."
},
"bootstrap": {
"title": "VoiceStudio",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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": "应用",
+3 -1
View File
@@ -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",
+24
View File
@@ -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;
}
+24
View File
@@ -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/);
});
});