diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs
index 1d37e639..ee24582d 100644
--- a/openless-all/app/src-tauri/src/lib.rs
+++ b/openless-all/app/src-tauri/src/lib.rs
@@ -3332,16 +3332,16 @@ fn capsule_window_bounds(translation_active: bool) -> CapsuleWindowBounds {
fn capsule_window_bounds_for_style(style: types::CapsuleStyle) -> CapsuleWindowBounds {
CapsuleWindowBounds {
- // typeless 窗口面积是旧尺寸(460×128)的 1/7;前端用 CSS zoom 同步缩放内容,
+ // typeless 窗口面积是原尺寸(460×128)的 1/5;前端用 CSS zoom 同步缩放内容,
// 见 CapsuleStyles.css 与 src/lib/capsuleLayout.ts。
width: match style {
- types::CapsuleStyle::Typeless => 174.0,
+ types::CapsuleStyle::Typeless => 206.0,
types::CapsuleStyle::Siri | types::CapsuleStyle::Classic => 460.0,
},
height: match style {
types::CapsuleStyle::Siri => 180.0,
types::CapsuleStyle::Classic => 100.0,
- types::CapsuleStyle::Typeless => 48.0,
+ types::CapsuleStyle::Typeless => 57.0,
},
bottom_inset: 0.0,
}
@@ -3599,9 +3599,9 @@ mod tests {
}
#[test]
- fn typeless_capsule_window_is_one_seventh_of_the_old_area() {
+ fn typeless_capsule_window_is_one_fifth_of_the_old_area() {
let bounds = capsule_window_bounds_for_style(CapsuleStyle::Typeless);
- assert_eq!((bounds.width, bounds.height), (174.0, 48.0));
+ assert_eq!((bounds.width, bounds.height), (206.0, 57.0));
}
#[test]
diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx
index 36f0ecb3..289e0f1f 100644
--- a/openless-all/app/src/components/Capsule.tsx
+++ b/openless-all/app/src/components/Capsule.tsx
@@ -6,12 +6,12 @@ import {
useMemo,
useRef,
useState,
- type CSSProperties,
type ReactNode,
} from 'react';
import { useTranslation } from 'react-i18next';
import { detectOS, type OS } from './WindowChrome';
-import { SiriGL, warmUpSiriShaders } from './SiriGL';
+import { warmUpSiriShaders } from './SiriGL';
+import { VoiceOrbStage } from './VoiceOrbStage';
import { TypelessCapsule } from './TypelessCapsule';
import { getSettings } from '../lib/ipc/settings';
import { cancelDictation, stopDictation } from '../lib/ipc/dictation';
@@ -79,123 +79,6 @@ if (typeof document !== 'undefined' && !document.getElementById('capsule-keyfram
document.head.appendChild(tag);
}
-interface VoiceOrbStageProps {
- os: OS;
- state: CapsuleState;
- level: number;
- /** 预备态:录音光条渲染成「待命」呼吸形态,不接真实电平。见 CapsulePayload.warming。 */
- warming?: boolean;
- /** 预备→就绪的平均耗时(ms),驱动展开动画的预测节奏。见 SiriGL warmupMs。 */
- warmupMs?: number;
- message?: string;
-}
-
-/**
- * 纯光效舞台(siri-glsl 完整克隆,无壳无按钮无底):
- * - recording:彩虹光谱声波横贯舞台,振幅随真实麦克风电平起伏;
- * - transcribing / polishing:波形从两端向中间收缩汇聚,流体圆点环淡入加速转动;
- * - done / cancelled:转速回落标准,六点合并成中央一颗圆,由外层 capsule-out 淡出;
- * - error:冻结光效 + 浮一行发光红字说明原因(唯一保留的文字信息)。
- * 刻意没有任何垫底/暗晕(用户拍板):白底界面上宁可对比度弱,也不要黑色遮挡。
- */
-function VoiceOrbStage({ os, state, level, warming, warmupMs, message }: VoiceOrbStageProps) {
- const { t } = useTranslation();
- const metrics = useMemo(() => getCapsulePillMetrics(os), [os]);
-
- // done / cancelled / error 冻结最后形态淡出,不再切换 phase。
- const lastPhaseRef = useRef<'wave' | 'orb'>('wave');
- let phase = lastPhaseRef.current;
- if (state === 'recording') phase = 'wave';
- else if (state === 'transcribing' || state === 'polishing') phase = 'orb';
- lastPhaseRef.current = phase;
- const isOrb = phase === 'orb';
-
- // 性能:波形淡出彻底结束(.55s delay + .6s duration)后卸载它的绘制循环 ——
- // 思考期间不再为一块不可见的 canvas 每帧跑 fragment。回到录音态立即重挂
- //(shader 编译已被驱动缓存,重建近零耗时)。
- const [waveAlive, setWaveAlive] = useState(true);
- useEffect(() => {
- if (!isOrb) {
- setWaveAlive(true);
- return undefined;
- }
- const timer = setTimeout(() => setWaveAlive(false), 1300);
- return () => clearTimeout(timer);
- }, [isOrb]);
-
- return (
-
- {waveAlive && (
-
- )}
- {isOrb && (
-
- )}
- {state === 'error' && {message || t('capsule.error')}}
-
- );
-}
-
-const errorGlowTextStyle: CSSProperties = {
- position: 'absolute',
- bottom: 24,
- left: '50%',
- transform: 'translateX(-50%)',
- maxWidth: 400,
- fontSize: 12,
- fontWeight: 600,
- lineHeight: 1.4,
- textAlign: 'center',
- color: 'var(--ol-err)',
- padding: '6px 12px',
- background: 'var(--ol-capsule-pill-bg)',
- border: '1px solid var(--ol-capsule-pill-border)',
- borderRadius: 12,
- whiteSpace: 'nowrap',
- overflow: 'hidden',
- textOverflow: 'ellipsis',
-};
-
interface SelectionPolishNoticeProps {
state: CapsuleState;
message?: string;
diff --git a/openless-all/app/src/components/CapsuleStyles.css b/openless-all/app/src/components/CapsuleStyles.css
index 63ab99c0..d8fe3f44 100644
--- a/openless-all/app/src/components/CapsuleStyles.css
+++ b/openless-all/app/src/components/CapsuleStyles.css
@@ -1,16 +1,21 @@
.ol-typeless-capsule-wrap {
- --ol-typeless-bg: #fafafa;
- --ol-typeless-border: #d4d4d8;
- --ol-typeless-ink: #18181b;
- --ol-typeless-button-bg: #e4e4e7;
- --ol-typeless-button-hover: #d4d4d8;
- --ol-typeless-confirm-bg: #27272a;
- --ol-typeless-confirm-ink: #ffffff;
+ --ol-typeless-bg: #18181b;
+ --ol-typeless-border: #52525b;
+ --ol-typeless-ink: #fafafa;
+ --ol-typeless-button-bg: #3f3f46;
+ --ol-typeless-button-hover: #52525b;
+ --ol-typeless-confirm-bg: #fafafa;
+ --ol-typeless-confirm-ink: #18181b;
position: relative;
font-family: var(--ol-font-sans);
- /* 整体缩到 1/7 面积(线性 1/√7≈0.378),与 174×48 的胶囊窗口配套;
+ /* 整体缩放使药丸面积为原尺寸的 1/5(线性 1/√5≈0.447),与 206×57 的胶囊窗口配套;
尺寸基准见 src-tauri/src/lib.rs 的 capsule_window_bounds_for_style。 */
- zoom: 0.378;
+ zoom: 0.447;
+}
+
+/* 黑色胶囊配白色波纹。 */
+.ol-typeless-capsule .ol-capsule-waveform > span {
+ background: #ffffff;
}
.ol-typeless-capsule {
@@ -212,28 +217,22 @@
width: 60px;
transform: scale(0.7);
}
-.ol-siri-capsule-preview .ol-capsule-waveform {
- width: 160px;
- gap: 5px;
- filter: drop-shadow(0 0 6px #8b5cf680);
+/* Siri 预览 = 缩小的真实光效舞台(460×180 → ×0.5 = 230×90,正好放进 256×96 预览框)。 */
+.ol-siri-capsule-preview {
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
-.ol-siri-capsule-preview .ol-capsule-waveform > span {
- width: 8px;
- background: #a580ff;
-}
-.ol-siri-capsule-preview .ol-capsule-waveform > span:nth-child(-n + 4) {
- background: #65a5ff;
-}
-.ol-siri-capsule-preview .ol-capsule-waveform > span:nth-child(n + 8) {
- background: #fa84b3;
+.ol-siri-capsule-preview > div {
+ transform: scale(0.5);
}
[data-ol-theme='dark'] .ol-typeless-capsule-wrap {
- --ol-typeless-bg: #202024;
- --ol-typeless-border: #505056;
+ --ol-typeless-bg: #101014;
+ --ol-typeless-border: #3f3f46;
--ol-typeless-ink: #fafafa;
- --ol-typeless-button-bg: #414148;
- --ol-typeless-button-hover: #55555e;
+ --ol-typeless-button-bg: #27272a;
+ --ol-typeless-button-hover: #3f3f46;
--ol-typeless-confirm-bg: #fafafa;
--ol-typeless-confirm-ink: #18181b;
}
diff --git a/openless-all/app/src/components/TypelessCapsule.tsx b/openless-all/app/src/components/TypelessCapsule.tsx
index 9aeede94..b9d75f38 100644
--- a/openless-all/app/src/components/TypelessCapsule.tsx
+++ b/openless-all/app/src/components/TypelessCapsule.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { cancelDictation, stopDictation } from '../lib/ipc/dictation';
import type { CapsuleState, CapsuleStyle } from '../lib/types';
import { Icon } from './Icon';
+import { VoiceOrbStage } from './VoiceOrbStage';
import './CapsuleStyles.css';
interface TypelessCapsuleProps {
@@ -122,7 +123,7 @@ export function CapsuleStylePreview({ style }: { style: CapsuleStyle }) {
) : (
-
+
)}
diff --git a/openless-all/app/src/components/VoiceOrbStage.tsx b/openless-all/app/src/components/VoiceOrbStage.tsx
new file mode 100644
index 00000000..1f903cc1
--- /dev/null
+++ b/openless-all/app/src/components/VoiceOrbStage.tsx
@@ -0,0 +1,123 @@
+import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
+import { useTranslation } from 'react-i18next';
+import { SiriGL } from './SiriGL';
+import { getCapsulePillMetrics } from '../lib/capsuleLayout';
+import type { CapsuleState } from '../lib/types';
+import type { OS } from './WindowChrome';
+
+export interface VoiceOrbStageProps {
+ os: OS;
+ state: CapsuleState;
+ level: number;
+ /** 预备态:录音光条渲染成「待命」呼吸形态,不接真实电平。见 CapsulePayload.warming。 */
+ warming?: boolean;
+ /** 预备→就绪的平均耗时(ms),驱动展开动画的预测节奏。见 SiriGL warmupMs。 */
+ warmupMs?: number;
+ message?: string;
+}
+
+/**
+ * 纯光效舞台(siri-glsl 完整克隆,无壳无按钮无底):
+ * - recording:彩虹光谱声波横贯舞台,振幅随真实麦克风电平起伏;
+ * - transcribing / polishing:波形从两端向中间收缩汇聚,流体圆点环淡入加速转动;
+ * - done / cancelled:转速回落标准,六点合并成中央一颗圆,由外层 capsule-out 淡出;
+ * - error:冻结光效 + 浮一行发光红字说明原因(唯一保留的文字信息)。
+ * 刻意没有任何垫底/暗晕(用户拍板):白底界面上宁可对比度弱,也不要黑色遮挡。
+ */
+export function VoiceOrbStage({ os, state, level, warming, warmupMs, message }: VoiceOrbStageProps) {
+ const { t } = useTranslation();
+ const metrics = useMemo(() => getCapsulePillMetrics(os), [os]);
+
+ // done / cancelled / error 冻结最后形态淡出,不再切换 phase。
+ const lastPhaseRef = useRef<'wave' | 'orb'>('wave');
+ let phase = lastPhaseRef.current;
+ if (state === 'recording') phase = 'wave';
+ else if (state === 'transcribing' || state === 'polishing') phase = 'orb';
+ lastPhaseRef.current = phase;
+ const isOrb = phase === 'orb';
+
+ // 性能:波形淡出彻底结束(.55s delay + .6s duration)后卸载它的绘制循环 ——
+ // 思考期间不再为一块不可见的 canvas 每帧跑 fragment。回到录音态立即重挂
+ //(shader 编译已被驱动缓存,重建近零耗时)。
+ const [waveAlive, setWaveAlive] = useState(true);
+ useEffect(() => {
+ if (!isOrb) {
+ setWaveAlive(true);
+ return undefined;
+ }
+ const timer = setTimeout(() => setWaveAlive(false), 1300);
+ return () => clearTimeout(timer);
+ }, [isOrb]);
+
+ return (
+
+ {waveAlive && (
+
+ )}
+ {isOrb && (
+
+ )}
+ {state === 'error' && {message || t('capsule.error')}}
+
+ );
+}
+
+const errorGlowTextStyle: CSSProperties = {
+ position: 'absolute',
+ bottom: 24,
+ left: '50%',
+ transform: 'translateX(-50%)',
+ maxWidth: 400,
+ fontSize: 12,
+ fontWeight: 600,
+ lineHeight: 1.4,
+ textAlign: 'center',
+ color: 'var(--ol-err)',
+ padding: '6px 12px',
+ background: 'var(--ol-capsule-pill-bg)',
+ border: '1px solid var(--ol-capsule-pill-border)',
+ borderRadius: 12,
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+};
diff --git a/openless-all/app/src/lib/capsuleLayout.test.ts b/openless-all/app/src/lib/capsuleLayout.test.ts
index 5742a811..0919d1c0 100644
--- a/openless-all/app/src/lib/capsuleLayout.test.ts
+++ b/openless-all/app/src/lib/capsuleLayout.test.ts
@@ -86,8 +86,8 @@ for (const os of ['mac', 'win', 'linux'] as const) {
const classic = getCapsuleHostMetrics(os, false, 'classic');
const typeless = getCapsuleHostMetrics(os, true, 'typeless');
assertEqual(classic.height, 100, `${os}: classic uses the compact native window`);
- assertEqual(typeless.height, 48, `${os}: typeless window is 1/7 of the old 460x128 area`);
- assertEqual(typeless.width, 174, `${os}: typeless window keeps the 1/7 stage width`);
+ assertEqual(typeless.height, 57, `${os}: typeless window is 1/5 of the old 460x128 area`);
+ assertEqual(typeless.width, 206, `${os}: typeless window keeps the 1/5 stage width`);
assertEqual(
typeless.bottomInset,
0,
diff --git a/openless-all/app/src/lib/capsuleLayout.ts b/openless-all/app/src/lib/capsuleLayout.ts
index fb0bcee1..0b0f3558 100644
--- a/openless-all/app/src/lib/capsuleLayout.ts
+++ b/openless-all/app/src/lib/capsuleLayout.ts
@@ -31,10 +31,10 @@ const VOICE_ORB_STAGE_WIDTH = 460;
const VOICE_ORB_STAGE_HEIGHT = 180;
const VOICE_ORB_TEXT_WIDTH = 400;
-// typeless 窗口面积是旧尺寸(460×128)的 1/7;内容由 CapsuleStyles.css 的 zoom 缩放,
+// typeless 窗口面积是原尺寸(460×128)的 1/5;内容由 CapsuleStyles.css 的 zoom 缩放,
// 与 src-tauri/src/lib.rs 的 capsule_window_bounds_for_style 保持一致。
-const TYPELESS_STAGE_WIDTH = 174;
-const TYPELESS_STAGE_HEIGHT = 48;
+const TYPELESS_STAGE_WIDTH = 206;
+const TYPELESS_STAGE_HEIGHT = 57;
export function parseCapsuleStyle(value: unknown): CapsuleStyle | undefined {
return value === 'siri' || value === 'classic' || value === 'typeless' ? value : undefined;