Skip to content

Commit 07ba7d0

Browse files
authored
Merge pull request #2643 from bobleer/bob/remote-connect-ui-polish
feat(web): polish remote chat app surfaces
2 parents ecc969d + eeb34aa commit 07ba7d0

10 files changed

Lines changed: 421 additions & 65 deletions

File tree

src/web-ui/src/app/components/NavPanel/NavPanel.scss

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,16 +1646,37 @@ $_section-header-height: 22px;
16461646
display: flex;
16471647
flex-shrink: 0;
16481648
align-items: center;
1649-
gap: 6px;
1650-
margin-left: 2px;
1651-
color: var(--bf-appearance-token-color-success);
1649+
gap: 4px;
1650+
margin-left: 1px;
1651+
color: var(--bf-appearance-token-color-text-secondary);
16521652
}
16531653

16541654
.bitfun-nav-panel__footer-device-status-attached-group {
1655+
width: 20px;
1656+
height: 20px;
16551657
display: inline-flex;
16561658
align-items: center;
1659+
justify-content: center;
16571660
gap: 1px;
1661+
box-sizing: border-box;
1662+
border: 1px solid var(--bf-appearance-token-border-subtle);
1663+
border-radius: $size-radius-sm;
1664+
background: var(--bf-appearance-token-element-bg-subtle);
16581665
line-height: 1;
1666+
1667+
&[data-bf-device-kind='message-app'] {
1668+
border-color: color-mix(
1669+
in srgb,
1670+
var(--bf-appearance-token-color-success) 24%,
1671+
var(--bf-appearance-token-border-subtle)
1672+
);
1673+
background: color-mix(
1674+
in srgb,
1675+
var(--bf-appearance-token-color-success) 9%,
1676+
var(--bf-appearance-token-element-bg-subtle)
1677+
);
1678+
color: var(--bf-appearance-token-color-success);
1679+
}
16591680
}
16601681

16611682
.bitfun-nav-panel__footer-device-status-attached-count {
@@ -2134,15 +2155,19 @@ $_section-header-height: 22px;
21342155

21352156
// Device overview v2: separate the device roles from the connection service.
21362157
.bitfun-device-overview {
2137-
width: min(344px, calc(100vw - 16px));
2158+
width: min(336px, calc(100vw - 16px));
21382159
}
21392160

21402161
.bitfun-device-overview__header {
2141-
min-height: 28px;
2162+
min-height: 24px;
2163+
padding: 1px 4px 2px;
21422164

21432165
.bitfun-device-overview__title {
21442166
overflow: hidden;
2145-
text-align: center;
2167+
color: var(--bf-appearance-token-color-text-secondary);
2168+
font-size: var(--bf-appearance-token-font-size-xs);
2169+
font-weight: 600;
2170+
text-align: left;
21462171
text-overflow: ellipsis;
21472172
white-space: nowrap;
21482173
}
@@ -2155,9 +2180,11 @@ $_section-header-height: 22px;
21552180
.bitfun-device-overview__local-device {
21562181
display: flex;
21572182
align-items: center;
2158-
gap: 11px;
2159-
min-height: 32px;
2160-
padding: 2px 4px;
2183+
gap: 10px;
2184+
min-height: 40px;
2185+
padding: 4px 6px;
2186+
border-radius: $size-radius-sm;
2187+
background: var(--bf-appearance-token-element-bg-subtle);
21612188

21622189
svg {
21632190
flex-shrink: 0;
@@ -2176,14 +2203,14 @@ $_section-header-height: 22px;
21762203
}
21772204

21782205
.bitfun-device-overview__device-group {
2179-
margin-top: 18px;
2206+
margin-top: 16px;
21802207

21812208
&.is-primary {
21822209
margin-top: 2px;
21832210
}
21842211

21852212
h3 {
2186-
margin: 0 4px 7px;
2213+
margin: 0 6px 7px;
21872214
color: var(--bf-appearance-token-color-text-muted);
21882215
font-size: 10px;
21892216
font-weight: 600;
@@ -2194,19 +2221,29 @@ $_section-header-height: 22px;
21942221
.bitfun-device-overview__device-rows {
21952222
display: flex;
21962223
flex-direction: column;
2197-
gap: 3px;
2224+
gap: 4px;
21982225
}
21992226

22002227
.bitfun-device-overview__device-row {
22012228
display: grid;
2202-
grid-template-columns: 18px minmax(0, 1fr) auto;
2229+
grid-template-columns: 30px minmax(0, 1fr) auto;
22032230
align-items: center;
22042231
gap: 10px;
2205-
min-height: 32px;
2206-
padding: 2px 4px;
2232+
min-height: 42px;
2233+
padding: 4px 6px;
2234+
border-radius: $size-radius-sm;
22072235

2208-
svg {
2209-
color: var(--bf-appearance-token-color-text-primary);
2236+
.is-primary & {
2237+
background: var(--bf-appearance-token-element-bg-subtle);
2238+
}
2239+
2240+
&[data-bf-device-kind='message-app'] .bitfun-device-overview__device-icon {
2241+
background: color-mix(
2242+
in srgb,
2243+
var(--bf-appearance-token-color-success) 10%,
2244+
var(--bf-appearance-token-element-bg-subtle)
2245+
);
2246+
color: var(--bf-appearance-token-color-success);
22102247
}
22112248

22122249
strong {
@@ -2244,6 +2281,22 @@ $_section-header-height: 22px;
22442281
}
22452282
}
22462283

2284+
.bitfun-device-overview__device-icon {
2285+
width: 28px;
2286+
height: 28px;
2287+
display: inline-flex;
2288+
flex-shrink: 0;
2289+
align-items: center;
2290+
justify-content: center;
2291+
border-radius: $size-radius-sm;
2292+
background: var(--bf-appearance-token-element-bg-soft);
2293+
color: var(--bf-appearance-token-color-text-primary);
2294+
2295+
svg {
2296+
flex-shrink: 0;
2297+
}
2298+
}
2299+
22472300
.bitfun-device-overview__service {
22482301
display: grid;
22492302
grid-template-columns: 16px auto minmax(0, 1fr);
@@ -2281,7 +2334,13 @@ $_section-header-height: 22px;
22812334
}
22822335

22832336
.bitfun-device-overview__actions {
2284-
padding: 0 4px;
2337+
margin-top: 2px;
2338+
padding: 12px 4px 0;
2339+
border-top: 1px solid var(--bf-appearance-token-border-subtle);
2340+
2341+
> [data-bf-component='button'] {
2342+
min-height: 34px;
2343+
}
22852344
}
22862345

22872346
.bitfun-nav-panel__remote-disclaimer {

src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/Ap
77
import { useAnchoredPopoverPosition } from '@/shared/utils/useAnchoredPopoverPosition';
88
import { usePeerDeviceModeOptional } from '@/infrastructure/peer-device/peerDeviceContextState';
99
import { useNotification } from '@/shared/notification-system';
10+
import {
11+
ChatAppBrandIcon,
12+
type ChatAppBrand,
13+
} from '../../RemoteConnectDialog/ChatAppBrandIcon';
1014
import {
1115
selectActivityFacts,
1216
selectAttachedGroups,
@@ -23,15 +27,36 @@ interface DeviceStatusControlProps {
2327
onManageDevices: () => void;
2428
}
2529

26-
function DeviceIcon({ kind, size = 17 }: { kind: DeviceOverviewDeviceKind; size?: number }) {
30+
/** Resolve the provider from backend ids, aliases, or display names. */
31+
function chatAppBrandFromIdentity(identity: string | null | undefined): ChatAppBrand | null {
32+
const normalized = identity?.trim().toLocaleLowerCase();
33+
if (!normalized) return null;
34+
if (normalized.includes('telegram')) return 'telegram';
35+
if (normalized.includes('feishu') || normalized.includes('lark')) return 'feishu';
36+
if (normalized.includes('weixin') || normalized.includes('wechat')) return 'weixin';
37+
return null;
38+
}
39+
40+
function DeviceIcon({
41+
identity,
42+
kind,
43+
size = 17,
44+
}: {
45+
identity?: string | null;
46+
kind: DeviceOverviewDeviceKind;
47+
size?: number;
48+
}) {
2749
const catalogSize = size <= 11 ? '2xs' : size <= 13 ? 'xs' : size <= 15 ? 'sm' : size <= 17 ? 'md' : 'lg';
2850
switch (kind) {
2951
case 'mobile':
3052
return <Smartphone size={size} aria-hidden="true" />;
3153
case 'execution-host':
3254
return <Server size={size} aria-hidden="true" />;
33-
case 'message-app':
55+
case 'message-app': {
56+
const chatApp = chatAppBrandFromIdentity(identity);
57+
if (chatApp) return <ChatAppBrandIcon app={chatApp} size={size} />;
3458
return <Icon name="side-chat" size={catalogSize} aria-hidden="true" />;
59+
}
3560
default:
3661
return <Monitor size={size} aria-hidden="true" />;
3762
}
@@ -131,6 +156,9 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
131156
[activityFactSentence, overview],
132157
);
133158
const attachedGroups = useMemo(() => selectAttachedGroups(overview), [overview]);
159+
const attachedMessageAppIdentity = useMemo(() => (
160+
overview.connectedDevices.find(device => device.kind === 'message-app')?.name
161+
), [overview.connectedDevices]);
134162
const accessibleSummary = [overview.currentWorkDeviceName, ...activityLines].join(' · ');
135163

136164
const deviceActivity = useCallback((device: DeviceOverviewDevice) => {
@@ -149,6 +177,16 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
149177
return parts.join(' · ');
150178
}, [t]);
151179

180+
const deviceDisplayName = useCallback((device: DeviceOverviewDevice) => {
181+
const chatApp = device.kind === 'message-app'
182+
? chatAppBrandFromIdentity(`${device.id} ${device.name}`)
183+
: null;
184+
if (chatApp === 'telegram') return 'Telegram';
185+
if (chatApp === 'feishu') return t('remoteConnect.feishu');
186+
if (chatApp === 'weixin') return t('remoteConnect.weixin');
187+
return device.name;
188+
}, [t]);
189+
152190
const serviceContent = useMemo(() => {
153191
const service = overview.connectionService;
154192
if (!service) return null;
@@ -196,9 +234,14 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
196234
{attachedGroups.map(group => (
197235
<span
198236
className="bitfun-nav-panel__footer-device-status-attached-group"
237+
data-bf-device-kind={group.kind}
199238
key={group.kind}
200239
>
201-
<DeviceIcon kind={group.kind} size={13} />
240+
<DeviceIcon
241+
identity={group.kind === 'message-app' ? attachedMessageAppIdentity : null}
242+
kind={group.kind}
243+
size={13}
244+
/>
202245
{group.count > 1 && (
203246
<span className="bitfun-nav-panel__footer-device-status-attached-count">
204247
{group.count}
@@ -238,9 +281,7 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
238281
}}
239282
>
240283
<CardHeader
241-
align="center"
242284
className="bitfun-device-overview__header"
243-
contentAlign="center"
244285
title={<h2 className="bitfun-device-overview__title">{t('deviceOverview.title')}</h2>}
245286
/>
246287
<ScrollArea className="bitfun-device-overview__scroll">
@@ -250,15 +291,27 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
250291
className="bitfun-device-overview__local-device"
251292
data-testid="nav-device-status-summary"
252293
>
253-
<DeviceIcon kind={overview.primaryDevice.kind} size={19} />
294+
<span className="bitfun-device-overview__device-icon" aria-hidden="true">
295+
<DeviceIcon
296+
identity={`${overview.primaryDevice.id} ${overview.primaryDevice.name}`}
297+
kind={overview.primaryDevice.kind}
298+
size={17}
299+
/>
300+
</span>
254301
<strong>{overview.currentWorkDeviceName}</strong>
255302
</div>
256303
) : (
257304
<>
258305
<section className="bitfun-device-overview__device-group is-primary">
259306
<h3>{t('deviceOverview.currentUse')}</h3>
260307
<div className="bitfun-device-overview__device-row">
261-
<DeviceIcon kind={overview.primaryDevice.kind} />
308+
<span className="bitfun-device-overview__device-icon" aria-hidden="true">
309+
<DeviceIcon
310+
identity={`${overview.primaryDevice.id} ${overview.primaryDevice.name}`}
311+
kind={overview.primaryDevice.kind}
312+
size={16}
313+
/>
314+
</span>
262315
<strong>{overview.primaryDevice.name}</strong>
263316
{overview.primaryDevice.activities.includes('background-execution') && (
264317
<span>{deviceActivity(overview.primaryDevice)}</span>
@@ -278,8 +331,14 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
278331
data-bf-device-kind={device.kind}
279332
data-bf-activities={device.activities.join(' ')}
280333
>
281-
<DeviceIcon kind={device.kind} />
282-
<strong>{device.name}</strong>
334+
<span className="bitfun-device-overview__device-icon" aria-hidden="true">
335+
<DeviceIcon
336+
identity={`${device.id} ${device.name}`}
337+
kind={device.kind}
338+
size={16}
339+
/>
340+
</span>
341+
<strong>{deviceDisplayName(device)}</strong>
283342
<span>{deviceActivity(device)}</span>
284343
</div>
285344
))}
@@ -321,9 +380,9 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
321380

322381
<CardFooter align="center" className="bitfun-device-overview__actions">
323382
<Button
324-
variant="fill"
383+
variant="outline"
325384
size="sm"
326-
leadingIcon={<Icon name="link" size="lg" />}
385+
leadingIcon={<Icon name="link" size="md" />}
327386
onClick={handleManageDevices}
328387
data-testid="nav-device-status-manage"
329388
>

src/web-ui/src/app/components/RemoteConnectDialog/ChatAppBrandIcon.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
export type ChatAppBrand = 'telegram' | 'feishu' | 'weixin';
2+
13
interface ChatAppBrandIconProps {
2-
app: 'telegram' | 'feishu' | 'weixin';
4+
app: ChatAppBrand;
35
size?: number;
46
}
57

src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.contract.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ const chatAppBrandIconSource = readFileSync(
1313
new URL('./ChatAppBrandIcon.tsx', import.meta.url),
1414
'utf8',
1515
);
16+
const deviceStatusControlSource = readFileSync(
17+
new URL('../NavPanel/components/DeviceStatusControl.tsx', import.meta.url),
18+
'utf8',
19+
);
1620
const accountPanelSource = readFileSync(
1721
new URL('./AccountPanel.tsx', import.meta.url),
1822
'utf8',
@@ -98,10 +102,14 @@ describe('Remote Connect safety contracts', () => {
98102

99103
it('uses the real monochrome app marks for every chat provider', () => {
100104
expect(dialogSource).toContain('<ChatAppBrandIcon app={botTab} size={28} />');
105+
expect(dialogSource).toContain('bitfun-remote-connect__chat-brand-group');
106+
expect(dialogSource).toContain('<ChatAppBrandIcon app={brand} size={15} />');
101107
expect(chatAppBrandIconSource).toContain("app === 'telegram'");
102108
expect(chatAppBrandIconSource).toContain("app === 'feishu'");
103109
expect(chatAppBrandIconSource.match(/viewBox="0 0 24 24"/g)).toHaveLength(3);
104110
expect(chatAppBrandIconSource.match(/fill="currentColor"/g)).toHaveLength(5);
111+
expect(deviceStatusControlSource).toContain('chatAppBrandFromIdentity(identity)');
112+
expect(deviceStatusControlSource).toContain('<ChatAppBrandIcon app={chatApp} size={size} />');
105113
expect(dialogSource).not.toContain('<Send size={28} />');
106114
expect(dialogSource).not.toContain('<MessageSquareText size={28} />');
107115
expect(dialogSource).not.toContain('<MessagesSquare size={28} />');

0 commit comments

Comments
 (0)