From 3d64dc23494fc5b107cb36bc432d3505f04afb09 Mon Sep 17 00:00:00 2001 From: weikeyi Date: Tue, 5 May 2026 13:11:16 +0800 Subject: [PATCH 1/4] fix(capsule): center Windows pill within shadow inset --- openless-all/app/src/components/Capsule.tsx | 6 ++++-- openless-all/app/src/lib/capsuleLayout.test.ts | 16 ++++++++++++++++ openless-all/app/src/lib/capsuleLayout.ts | 13 +++++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index 4c669598..2c8cb987 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -318,7 +318,9 @@ export function Capsule() { position: 'relative', display: 'flex', alignItems: 'center', - justifyContent: os === 'win' ? 'flex-end' : 'center', + justifyContent: 'center', + paddingLeft: hostMetrics.horizontalInset, + paddingRight: hostMetrics.horizontalInset, paddingTop: os === 'win' ? Math.max(0, hostMetrics.height - metrics.height - hostMetrics.bottomInset) : 0, @@ -336,7 +338,7 @@ export function Capsule() { position: 'absolute', left: '50%', // macOS / Linux:胶囊窗口 220×110、pill 居中,badge 锚到 pill 中线上方 21+8。 - // Windows:pill 不居中(带 12pt 阴影 inset),用 hostMetrics 量到底部 inset + pill 高 + gap。 + // Windows:host 比 pill 多出左右 12px / 底部 12px 的阴影空间,pill 仍保持居中。 bottom: os === 'win' ? `${hostMetrics.bottomInset + metrics.height + hostMetrics.badgeGap}px` : 'calc(50% + 21px + 8px)', diff --git a/openless-all/app/src/lib/capsuleLayout.test.ts b/openless-all/app/src/lib/capsuleLayout.test.ts index 9fe675cb..2466301e 100644 --- a/openless-all/app/src/lib/capsuleLayout.test.ts +++ b/openless-all/app/src/lib/capsuleLayout.test.ts @@ -1,4 +1,5 @@ import { + getCapsuleHostMetrics, getCapsuleMessageLayout, getCapsulePillMetrics, } from './capsuleLayout.ts'; @@ -14,6 +15,21 @@ assertEqual(winMetrics.width, 196, 'windows capsule widens pill'); assertEqual(winMetrics.height, 52, 'windows capsule increases pill height'); assertEqual(winMetrics.textWidth, 118, 'windows capsule widens text slot'); +const winHost = getCapsuleHostMetrics('win', false); +assertEqual(winHost.width, 220, 'windows capsule host keeps the current outer hitbox width'); +assertEqual(winHost.height, 84, 'windows capsule host keeps regular height'); +assertEqual(winHost.horizontalInset, 12, 'windows capsule host keeps symmetric shadow insets'); +assertEqual( + winHost.width, + winMetrics.width + winHost.horizontalInset * 2, + 'windows capsule host width derives from pill width plus symmetric side insets', +); + +const winHostWithTranslation = getCapsuleHostMetrics('win', true); +assertEqual(winHostWithTranslation.width, 220, 'windows translation capsule keeps the same outer width'); +assertEqual(winHostWithTranslation.height, 118, 'windows translation capsule grows vertically only'); +assertEqual(winHostWithTranslation.horizontalInset, 12, 'windows translation capsule keeps symmetric side insets'); + const macMetrics = getCapsulePillMetrics('mac'); assertEqual(macMetrics.width, 176, 'mac capsule keeps existing pill width'); assertEqual(macMetrics.height, 42, 'mac capsule keeps existing pill height'); diff --git a/openless-all/app/src/lib/capsuleLayout.ts b/openless-all/app/src/lib/capsuleLayout.ts index 7e8c8db7..28bb4344 100644 --- a/openless-all/app/src/lib/capsuleLayout.ts +++ b/openless-all/app/src/lib/capsuleLayout.ts @@ -11,6 +11,7 @@ export interface CapsulePillMetrics { export interface CapsuleHostMetrics { width: number; height: number; + horizontalInset: number; bottomInset: number; badgeGap: number; } @@ -35,9 +36,17 @@ export function getCapsuleHostMetrics( translationActive: boolean, ): CapsuleHostMetrics { if (os === 'win') { - return { width: 220, height: translationActive ? 118 : 84, bottomInset: 12, badgeGap: 8 }; + const horizontalInset = 12; + const pill = getCapsulePillMetrics(os); + return { + width: pill.width + horizontalInset * 2, + height: translationActive ? 118 : 84, + horizontalInset, + bottomInset: 12, + badgeGap: 8, + }; } - return { width: 176, height: 42, bottomInset: 0, badgeGap: 8 }; + return { width: 176, height: 42, horizontalInset: 0, bottomInset: 0, badgeGap: 8 }; } export function getCapsuleMessageLayout( From 9660db6336c546cd034c5e09a7d3f9b96f1af952 Mon Sep 17 00:00:00 2001 From: weikeyi Date: Tue, 5 May 2026 16:48:36 +0800 Subject: [PATCH 2/4] Fix Windows capsule box sizing --- openless-all/app/src-tauri/src/lib.rs | 6 +++++- openless-all/app/src/components/Capsule.tsx | 2 ++ openless-all/app/src/lib/capsuleLayout.test.ts | 9 +++++++++ openless-all/app/src/lib/capsuleLayout.ts | 17 ++++++++++++++--- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 1fe64723..0a3256d7 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -752,8 +752,12 @@ struct CapsuleWindowBounds { fn capsule_window_bounds(translation_active: bool) -> CapsuleWindowBounds { #[cfg(target_os = "windows")] { + const WINDOWS_CAPSULE_PILL_WIDTH: f64 = 196.0; + const WINDOWS_CAPSULE_SIDE_INSET: f64 = 12.0; CapsuleWindowBounds { - width: 220.0, + // Keep the existing Windows hitbox width, but express it as + // pill width (196) + symmetric 12px side insets for shadow room. + width: WINDOWS_CAPSULE_PILL_WIDTH + WINDOWS_CAPSULE_SIDE_INSET * 2.0, height: if translation_active { 118.0 } else { 84.0 }, bottom_inset: 12.0, } diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index 2c8cb987..02430b96 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -236,6 +236,7 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }: padding: '0 8px', width: metrics.width, height: metrics.height, + boxSizing: metrics.boxSizing, borderRadius: 999, background: 'rgba(255, 255, 255, 0.62)', backdropFilter: 'blur(28px) saturate(180%)', @@ -321,6 +322,7 @@ export function Capsule() { justifyContent: 'center', paddingLeft: hostMetrics.horizontalInset, paddingRight: hostMetrics.horizontalInset, + boxSizing: hostMetrics.boxSizing, paddingTop: os === 'win' ? Math.max(0, hostMetrics.height - metrics.height - hostMetrics.bottomInset) : 0, diff --git a/openless-all/app/src/lib/capsuleLayout.test.ts b/openless-all/app/src/lib/capsuleLayout.test.ts index 2466301e..f2f97296 100644 --- a/openless-all/app/src/lib/capsuleLayout.test.ts +++ b/openless-all/app/src/lib/capsuleLayout.test.ts @@ -14,26 +14,35 @@ const winMetrics = getCapsulePillMetrics('win'); assertEqual(winMetrics.width, 196, 'windows capsule widens pill'); assertEqual(winMetrics.height, 52, 'windows capsule increases pill height'); assertEqual(winMetrics.textWidth, 118, 'windows capsule widens text slot'); +assertEqual(winMetrics.boxSizing, 'border-box', 'windows capsule pill width is an outer border-box metric'); const winHost = getCapsuleHostMetrics('win', false); assertEqual(winHost.width, 220, 'windows capsule host keeps the current outer hitbox width'); assertEqual(winHost.height, 84, 'windows capsule host keeps regular height'); assertEqual(winHost.horizontalInset, 12, 'windows capsule host keeps symmetric shadow insets'); +assertEqual(winHost.boxSizing, 'border-box', 'windows capsule host inset is reserved inside the native width'); assertEqual( winHost.width, winMetrics.width + winHost.horizontalInset * 2, 'windows capsule host width derives from pill width plus symmetric side insets', ); +assertEqual( + winHost.width - winHost.horizontalInset * 2, + winMetrics.width, + 'windows capsule host keeps the visible pill width after reserving side insets', +); const winHostWithTranslation = getCapsuleHostMetrics('win', true); assertEqual(winHostWithTranslation.width, 220, 'windows translation capsule keeps the same outer width'); assertEqual(winHostWithTranslation.height, 118, 'windows translation capsule grows vertically only'); assertEqual(winHostWithTranslation.horizontalInset, 12, 'windows translation capsule keeps symmetric side insets'); +assertEqual(winHostWithTranslation.boxSizing, 'border-box', 'windows translation host keeps the same inset-reserving box model'); const macMetrics = getCapsulePillMetrics('mac'); assertEqual(macMetrics.width, 176, 'mac capsule keeps existing pill width'); assertEqual(macMetrics.height, 42, 'mac capsule keeps existing pill height'); assertEqual(macMetrics.textWidth, 84, 'mac capsule keeps existing text slot'); +assertEqual(macMetrics.boxSizing, 'content-box', 'mac capsule keeps the existing content-box pill model'); const winErrorLayout = getCapsuleMessageLayout('win', 'error'); assertEqual(winErrorLayout.lineClamp, 2, 'windows error message allows two lines'); diff --git a/openless-all/app/src/lib/capsuleLayout.ts b/openless-all/app/src/lib/capsuleLayout.ts index 28bb4344..c0b22fcf 100644 --- a/openless-all/app/src/lib/capsuleLayout.ts +++ b/openless-all/app/src/lib/capsuleLayout.ts @@ -6,6 +6,7 @@ export interface CapsulePillMetrics { width: number; height: number; textWidth: number; + boxSizing: 'border-box' | 'content-box'; } export interface CapsuleHostMetrics { @@ -14,6 +15,7 @@ export interface CapsuleHostMetrics { horizontalInset: number; bottomInset: number; badgeGap: number; + boxSizing: 'border-box' | 'content-box'; } export interface CapsuleMessageLayout { @@ -23,10 +25,11 @@ export interface CapsuleMessageLayout { export function getCapsulePillMetrics(os: OS): CapsulePillMetrics { if (os === 'win') { - return { width: 196, height: 52, textWidth: 118 }; + // Windows metrics describe the visible outer footprint of the pill. + return { width: 196, height: 52, textWidth: 118, boxSizing: 'border-box' }; } - return { width: 176, height: 42, textWidth: 84 }; + return { width: 176, height: 42, textWidth: 84, boxSizing: 'content-box' }; } // macOS 走 1.2.11 calc 布局,不依赖 host metrics;Windows 端要更大的 host @@ -44,9 +47,17 @@ export function getCapsuleHostMetrics( horizontalInset, bottomInset: 12, badgeGap: 8, + boxSizing: 'border-box', }; } - return { width: 176, height: 42, horizontalInset: 0, bottomInset: 0, badgeGap: 8 }; + return { + width: 176, + height: 42, + horizontalInset: 0, + bottomInset: 0, + badgeGap: 8, + boxSizing: 'content-box', + }; } export function getCapsuleMessageLayout( From 94e9686a45f94283ab350350e654595c3fee3e90 Mon Sep 17 00:00:00 2001 From: weikeyi Date: Wed, 6 May 2026 23:18:16 +0800 Subject: [PATCH 3/4] fix(capsule): restore non-windows box sizing contract --- .../app/scripts/windows-ui-config.test.mjs | 16 ++++++++++------ openless-all/app/src/lib/capsuleLayout.test.ts | 5 ++++- openless-all/app/src/lib/capsuleLayout.ts | 4 ++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/openless-all/app/scripts/windows-ui-config.test.mjs b/openless-all/app/scripts/windows-ui-config.test.mjs index 873afdbb..9bdad4dd 100644 --- a/openless-all/app/scripts/windows-ui-config.test.mjs +++ b/openless-all/app/scripts/windows-ui-config.test.mjs @@ -55,7 +55,7 @@ if (!/function WindowsResizeHandles\(\)/.test(windowChromeTsx)) { assertMatch( windowChromeTsx, - /const MAC_TITLEBAR_HEIGHT = 30;/, + /const MAC_TITLEBAR_HEIGHT = 28;/, 'macOS titlebar spacer should stay visually compact around the native traffic lights', ); assertMatch( @@ -108,7 +108,7 @@ if (!/export function getCapsuleHostMetrics\(\s*os: OS,\s*translationActive: boo throw new Error('capsule layout should define explicit host metrics separate from the visible pill metrics'); } -if (!/if \(os === 'win'\)\s*\{[\s\S]*?width: 220,[\s\S]*?height: translationActive \? 118 : 84,[\s\S]*?bottomInset: 12,[\s\S]*?badgeGap: 8[\s\S]*?\}/.test(capsuleLayoutTs)) { +if (!/if \(os === 'win'\)\s*\{[\s\S]*?const horizontalInset = 12;[\s\S]*?const pill = getCapsulePillMetrics\(os\);[\s\S]*?width: pill\.width \+ horizontalInset \* 2,[\s\S]*?height: translationActive \? 118 : 84,[\s\S]*?horizontalInset,[\s\S]*?bottomInset: 12,[\s\S]*?badgeGap: 8,[\s\S]*?boxSizing: 'border-box',[\s\S]*?\}/.test(capsuleLayoutTs)) { throw new Error('windows capsule host metrics should leave room for shadow and badge geometry'); } @@ -116,19 +116,23 @@ if (!/const hostMetrics = getCapsuleHostMetrics\(os,\s*translation\);/.test(caps throw new Error('capsule should derive host metrics from the shared layout contract'); } -if (!/justifyContent:\s*os === 'win' \? 'flex-end' : 'center'/.test(capsuleTsx)) { - throw new Error('windows capsule host should anchor the pill to the bottom instead of centering it inside the larger native host window'); +if (!/justifyContent:\s*'center'/.test(capsuleTsx)) { + throw new Error('capsule host should center the pill within the shared layout contract'); +} + +if (!/paddingLeft:\s*hostMetrics\.horizontalInset,/.test(capsuleTsx) || !/paddingRight:\s*hostMetrics\.horizontalInset,/.test(capsuleTsx)) { + throw new Error('windows capsule host should reserve shared horizontal inset room for shadow geometry'); } if (!/paddingBottom:\s*os === 'win' \? hostMetrics\.bottomInset : 0/.test(capsuleTsx)) { throw new Error('windows capsule host should respect the shared bottom inset'); } -if (!/bottom:\s*`\$\{hostMetrics\.bottomInset \+ metrics\.height \+ hostMetrics\.badgeGap\}px`/.test(capsuleTsx)) { +if (!/hostMetrics\.bottomInset \+ metrics\.height \+ hostMetrics\.badgeGap/.test(capsuleTsx)) { throw new Error('windows translation badge should anchor from the shared host inset instead of a fixed center-based offset'); } -if (!/#\[cfg\(target_os = "windows"\)\][\s\S]*?width: 220\.0[\s\S]*?height: if translation_active \{ 118\.0 \} else \{ 84\.0 \}[\s\S]*?bottom_inset: 12\.0,/.test(libRs)) { +if (!/#\[cfg\(target_os = "windows"\)\][\s\S]*?const WINDOWS_CAPSULE_PILL_WIDTH: f64 = 196\.0;[\s\S]*?const WINDOWS_CAPSULE_SIDE_INSET: f64 = 12\.0;[\s\S]*?width: WINDOWS_CAPSULE_PILL_WIDTH \+ WINDOWS_CAPSULE_SIDE_INSET \* 2\.0,[\s\S]*?height: if translation_active \{ 118\.0 \} else \{ 84\.0 \},[\s\S]*?bottom_inset: 12\.0,/.test(libRs)) { throw new Error('windows runtime capsule bounds should leave room for the native shadow while keeping a fixed visual pill'); } diff --git a/openless-all/app/src/lib/capsuleLayout.test.ts b/openless-all/app/src/lib/capsuleLayout.test.ts index a55f0f92..c4a0f6c0 100644 --- a/openless-all/app/src/lib/capsuleLayout.test.ts +++ b/openless-all/app/src/lib/capsuleLayout.test.ts @@ -42,7 +42,10 @@ const macMetrics = getCapsulePillMetrics('mac'); assertEqual(macMetrics.width, 176, 'mac capsule keeps existing pill width'); assertEqual(macMetrics.height, 42, 'mac capsule keeps existing pill height'); assertEqual(macMetrics.textWidth, 84, 'mac capsule keeps existing text slot'); -assertEqual(macMetrics.boxSizing, 'content-box', 'mac capsule keeps the existing content-box pill model'); +assertEqual(macMetrics.boxSizing, 'border-box', 'mac capsule keeps the existing border-box pill model'); + +const macHost = getCapsuleHostMetrics('mac', false); +assertEqual(macHost.boxSizing, 'border-box', 'mac capsule host keeps the existing border-box box model'); const winErrorLayout = getCapsuleMessageLayout('win', 'error'); assertEqual(winErrorLayout.lineClamp, 2, 'windows error message allows two lines'); diff --git a/openless-all/app/src/lib/capsuleLayout.ts b/openless-all/app/src/lib/capsuleLayout.ts index 2b0dbf59..b8f9207d 100644 --- a/openless-all/app/src/lib/capsuleLayout.ts +++ b/openless-all/app/src/lib/capsuleLayout.ts @@ -29,7 +29,7 @@ export function getCapsulePillMetrics(os: OS): CapsulePillMetrics { return { width: 196, height: 52, textWidth: 104, boxSizing: 'border-box' }; } - return { width: 176, height: 42, textWidth: 84, boxSizing: 'content-box' }; + return { width: 176, height: 42, textWidth: 84, boxSizing: 'border-box' }; } // macOS 走 1.2.11 calc 布局,不依赖 host metrics;Windows 端要更大的 host @@ -56,7 +56,7 @@ export function getCapsuleHostMetrics( horizontalInset: 0, bottomInset: 0, badgeGap: 8, - boxSizing: 'content-box', + boxSizing: 'border-box', }; } From 62005a63915bb8a47970669c84e43a6b3e7ec65e Mon Sep 17 00:00:00 2001 From: weikeyi Date: Wed, 6 May 2026 23:32:28 +0800 Subject: [PATCH 4/4] test(capsule): tighten host centering regex --- openless-all/app/scripts/windows-ui-config.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openless-all/app/scripts/windows-ui-config.test.mjs b/openless-all/app/scripts/windows-ui-config.test.mjs index 9bdad4dd..4d9ddb61 100644 --- a/openless-all/app/scripts/windows-ui-config.test.mjs +++ b/openless-all/app/scripts/windows-ui-config.test.mjs @@ -116,7 +116,7 @@ if (!/const hostMetrics = getCapsuleHostMetrics\(os,\s*translation\);/.test(caps throw new Error('capsule should derive host metrics from the shared layout contract'); } -if (!/justifyContent:\s*'center'/.test(capsuleTsx)) { +if (!/return\s*\(\s*