UI编辑器一批细节优化 #278
@@ -1754,8 +1754,6 @@ for (const snippet of [
|
||||
"'write_game_creator_app_config'",
|
||||
'aria-label="运行时配置"',
|
||||
'LLM API Key',
|
||||
'External Editor Base URL',
|
||||
'External Editor API Key',
|
||||
'runtime_config.save',
|
||||
"'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'",
|
||||
"'activate_local_game_preview'",
|
||||
@@ -1791,6 +1789,19 @@ for (const snippet of [
|
||||
}
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
"label: '连接与工具'",
|
||||
"id: 'connections'",
|
||||
"activeSection === 'connections'",
|
||||
'aria-label="外部连接"',
|
||||
]) {
|
||||
if (appSource.includes(snippet)) {
|
||||
throw new Error(
|
||||
`AI game creator shell removed settings page was reintroduced: ${snippet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const script of [
|
||||
'ai-game-creator-shell:dev',
|
||||
'ai-game-creator-shell:dev-server',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Bot,
|
||||
Cable,
|
||||
CheckCircle2,
|
||||
CircleAlert,
|
||||
Info,
|
||||
@@ -91,7 +90,6 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
|
||||
type RuntimeSettingsSection =
|
||||
| 'general'
|
||||
| 'agents'
|
||||
| 'connections'
|
||||
| 'extensions'
|
||||
| 'advanced'
|
||||
| 'about';
|
||||
@@ -116,12 +114,6 @@ const runtimeSettingsSections = [
|
||||
description: '按角色覆盖默认模型',
|
||||
icon: Bot,
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
label: '连接与工具',
|
||||
description: '外部服务',
|
||||
icon: Cable,
|
||||
},
|
||||
{
|
||||
id: 'extensions',
|
||||
label: '扩展',
|
||||
@@ -1452,57 +1444,6 @@ export function RuntimeConfigDialog({
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{activeSection === 'connections' ? (
|
||||
<section
|
||||
className="runtime-settings-section"
|
||||
aria-label="外部连接"
|
||||
>
|
||||
<header className="runtime-settings-section-header">
|
||||
<div>
|
||||
<h3>外部连接</h3>
|
||||
<p>配置图片编辑器使用的外部服务</p>
|
||||
</div>
|
||||
</header>
|
||||
{allowAdvancedExternalEditorConfig ? (
|
||||
<div className="runtime-settings-fields">
|
||||
<label>
|
||||
External Editor Base URL
|
||||
<input
|
||||
aria-label="External Editor Base URL"
|
||||
value={runtimeConfigDraft.editorApi.baseUrl}
|
||||
onChange={(event) =>
|
||||
setRuntimeConfigDraft((current) => ({
|
||||
...current,
|
||||
editorApi: {
|
||||
...current.editorApi,
|
||||
baseUrl: event.currentTarget.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
External Editor API Key
|
||||
<input
|
||||
aria-label="External Editor API Key"
|
||||
autoComplete="off"
|
||||
type="password"
|
||||
value={runtimeConfigDraft.editorApi.apiKey}
|
||||
onChange={(event) =>
|
||||
setRuntimeConfigDraft((current) => ({
|
||||
...current,
|
||||
editorApi: {
|
||||
...current.editorApi,
|
||||
apiKey: event.currentTarget.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
{activeSection === 'extensions' ? (
|
||||
<section
|
||||
className="runtime-settings-section runtime-settings-extensions"
|
||||
|
||||
+8
-1
@@ -514,7 +514,14 @@ export function PreviewWorkspace({
|
||||
{(actions) => (
|
||||
<div
|
||||
className="absolute bottom-3 right-3 z-10 flex items-center gap-1 rounded-lg border border-(--platform-subpanel-border) bg-white/90 p-1 shadow-lg"
|
||||
data-ui-editor-zoom-controls
|
||||
onMouseDown={(event) => {
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
event.target.closest('button')
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -21,11 +21,10 @@ function isEditableTarget(target: EventTarget | null) {
|
||||
|
||||
function isInteractiveTarget(target: EventTarget | null) {
|
||||
const element = target instanceof Element ? target : null;
|
||||
if (element?.closest('[data-ui-editor-zoom-controls]')) return false;
|
||||
return Boolean(
|
||||
(target instanceof HTMLElement && target.isContentEditable) ||
|
||||
element?.closest(
|
||||
'button, a, input:not([type="range"]), textarea, select, [contenteditable="true"], [role="button"], [role="dialog"], [aria-modal="true"]',
|
||||
'button, a, input, textarea, select, [contenteditable="true"], [role="button"], [role="dialog"], [aria-modal="true"]',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -832,7 +832,7 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
expect(
|
||||
screen.getByLabelText('LLM API Key').getAttribute('autocomplete'),
|
||||
).toBe('off');
|
||||
fireEvent.click(screen.getByRole('button', { name: /连接与工具/ }));
|
||||
expect(screen.queryByRole('button', { name: /连接与工具/ })).toBeNull();
|
||||
expect(screen.queryByLabelText('External Editor Base URL')).toBeNull();
|
||||
expect(screen.queryByLabelText('External Editor API Key')).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent 模型/ }));
|
||||
@@ -1007,7 +1007,7 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
target: { value: 'ark' },
|
||||
},
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /连接与工具/ }));
|
||||
expect(screen.queryByRole('button', { name: /连接与工具/ })).toBeNull();
|
||||
expect(screen.queryByLabelText('External Editor Base URL')).toBeNull();
|
||||
expect(screen.queryByLabelText('External Editor API Key')).toBeNull();
|
||||
fireEvent.click(
|
||||
@@ -1166,7 +1166,7 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
'value',
|
||||
'12000',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /连接与工具/ }));
|
||||
expect(screen.queryByRole('button', { name: /连接与工具/ })).toBeNull();
|
||||
expect(screen.queryByLabelText('External Editor Base URL')).toBeNull();
|
||||
expect(screen.queryByLabelText('External Editor API Key')).toBeNull();
|
||||
fireEvent.click(
|
||||
|
||||
@@ -60,30 +60,32 @@ describe('ui editor keyboard shortcuts', () => {
|
||||
window.removeEventListener('keydown', listener);
|
||||
});
|
||||
|
||||
it('keeps node deletion available when the zoom slider has focus', () => {
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
document.body.append(slider);
|
||||
const deleteNode = vi.fn(() => ({ ok: true }));
|
||||
const listener = (event: KeyboardEvent) =>
|
||||
handleUiEditorKeyDown(event, {
|
||||
selectedNodeId: 'node-1',
|
||||
activeImageId: 'page-1',
|
||||
deleteNode,
|
||||
historyUndo: vi.fn(() => true),
|
||||
historyRedo: vi.fn(() => true),
|
||||
});
|
||||
window.addEventListener('keydown', listener);
|
||||
it.each(['Delete', 'Backspace'])(
|
||||
'does not delete a node when the zoom slider has focus (%s)',
|
||||
(key) => {
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
document.body.append(slider);
|
||||
const deleteNode = vi.fn(() => ({ ok: true }));
|
||||
const listener = (event: KeyboardEvent) =>
|
||||
handleUiEditorKeyDown(event, {
|
||||
selectedNodeId: 'node-1',
|
||||
activeImageId: 'page-1',
|
||||
deleteNode,
|
||||
historyUndo: vi.fn(() => true),
|
||||
historyRedo: vi.fn(() => true),
|
||||
});
|
||||
window.addEventListener('keydown', listener);
|
||||
|
||||
fireEvent.keyDown(slider, { key: 'Delete' });
|
||||
fireEvent.keyDown(slider, { key });
|
||||
|
||||
expect(deleteNode).toHaveBeenCalledWith('node-1', 'page-1');
|
||||
window.removeEventListener('keydown', listener);
|
||||
});
|
||||
expect(deleteNode).not.toHaveBeenCalled();
|
||||
window.removeEventListener('keydown', listener);
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps node deletion available after a zoom button is clicked', () => {
|
||||
it('keeps node deletion available after zoom button focus is released', () => {
|
||||
const zoomControls = document.createElement('div');
|
||||
zoomControls.setAttribute('data-ui-editor-zoom-controls', '');
|
||||
const zoomIn = document.createElement('button');
|
||||
zoomControls.append(zoomIn);
|
||||
document.body.append(zoomControls);
|
||||
@@ -99,7 +101,7 @@ describe('ui editor keyboard shortcuts', () => {
|
||||
window.addEventListener('keydown', listener);
|
||||
|
||||
fireEvent.click(zoomIn);
|
||||
fireEvent.keyDown(zoomIn, { key: 'Delete' });
|
||||
fireEvent.keyDown(window, { key: 'Delete' });
|
||||
|
||||
expect(deleteNode).toHaveBeenCalledWith('node-1', 'page-1');
|
||||
window.removeEventListener('keydown', listener);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- 保留 `−` 与 `+` 按钮。点击轨道可跳转,拖动滑块实时更新画布和百分比。
|
||||
- 缩放以视口中心为中心,继续支持 Ctrl/Cmd + 滚轮缩放。
|
||||
- 滑杆支持方向键、Home、End;百分比文本可点击编辑并在失焦时限制到有效范围。
|
||||
- 缩放滑杆即使处于焦点,也不得拦截 UI 节点的 Delete/Backspace 快捷删除。
|
||||
- 缩放滑杆获得焦点时,Delete/Backspace 不触发 UI 节点快捷删除;缩放按钮鼠标点击不改变快捷键焦点。
|
||||
- Windows/Linux 使用 `Ctrl`,macOS 使用 `Cmd`;`+`、`=` 与 `NumpadAdd` 放大,`-` 与 `NumpadSubtract` 缩小,允许按键重复时连续缩放。
|
||||
- 快捷键直接复用现有 `+`/`−` 按钮动作:以视口中心为锚点,按 `×1.16`/`×0.86` 改变缩放。
|
||||
- 鼠标悬停在预览画布上,或预览画布已获得键盘焦点时,快捷键生效。预览画布需提供可聚焦语义与无障碍名称。
|
||||
|
||||
Reference in New Issue
Block a user