qwen-code/packages/web-shell/client/components/dialogs/ThemeDialog.tsx
ermin.zem 5c82857fea
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
Add harness infrastructure for web-shell package (#6517)
* test(web-shell): add browser and lint harness

* test(web-shell): harden browser smoke harness

* fix(web-shell): guard mock daemon model state

* test(web-shell): remove unused scenario harness

* fix(web-shell): remove stale lint disables

* test(web-shell): make matchMedia stub writable

* fix(web-shell): exclude tests from package typecheck

* test(web-shell): tighten mock daemon route contract

* Update packages/web-shell/client/e2e/utils/mockDaemon.ts

Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>

* test(web-shell): clear stale SSE connections

* ci(web-shell): gate smoke on full CI profile

---------

Co-authored-by: ermin.zem <ermin.zem@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-07-09 08:11:58 +00:00

95 lines
2.7 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { dp } from './dialogStyles';
import { useI18n } from '../../i18n';
import { useListboxKeyboard } from '../../hooks/useListboxKeyboard';
import { WEB_SHELL_THEMES, type WebShellTheme } from '../../themeContext';
interface ThemeDialogProps {
currentTheme: WebShellTheme;
onSelect: (theme: WebShellTheme) => void;
onClose: () => void;
}
export function ThemeDialog({
currentTheme,
onSelect,
onClose,
}: ThemeDialogProps) {
const { t } = useI18n();
const themes = WEB_SHELL_THEMES.map((id) => ({
id,
label: t(`theme.${id}`),
description: t(`theme.${id}.desc`),
}));
const [selectedIdx, setSelectedIdx] = useState(() => {
const idx = themes.findIndex((theme) => theme.id === currentTheme);
return idx >= 0 ? idx : 0;
});
const listRef = useRef<HTMLDivElement>(null);
const confirm = (index: number) => {
const theme = themes[index];
if (!theme) return;
onSelect(theme.id);
onClose();
};
const { keyboardMode } = useListboxKeyboard({
itemCount: themes.length,
activeIndex: selectedIdx,
onActiveIndexChange: setSelectedIdx,
onConfirm: confirm,
});
useEffect(() => {
const el = listRef.current?.children[selectedIdx] as
| HTMLElement
| undefined;
el?.scrollIntoView({ block: 'nearest' });
}, [selectedIdx]);
return (
<div
className={dp(
'picker-list',
'picker-list-compact',
keyboardMode ? 'picker-keyboard-only' : undefined,
)}
ref={listRef}
role="listbox"
aria-label={t('theme.title')}
tabIndex={0}
aria-activedescendant={
themes.length > 0 ? `theme-opt-${selectedIdx}` : undefined
}
data-web-shell-theme-dialog
>
{themes.map((theme, index) => {
const selected = theme.id === currentTheme;
return (
<div
key={theme.id}
id={`theme-opt-${index}`}
role="option"
aria-selected={selected}
className={dp(
'picker-item',
'picker-session-item',
index === selectedIdx ? 'selected' : undefined,
selected ? 'dialog-current' : undefined,
)}
data-web-shell-theme-option
data-theme-id={theme.id}
onClick={() => confirm(index)}
onMouseMove={() => setSelectedIdx(index)}
>
<div className={dp('picker-item-row')}>
<span className={dp('picker-item-title')}>{theme.label}</span>
</div>
<div className={dp('picker-item-meta')}>{theme.description}</div>
</div>
);
})}
</div>
);
}