Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/features/dashboard/dashboard-lib.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, expect, it } from 'vitest'

import { THEME_PREFERENCE_CHIPS, THEME_PREFERENCES } from './dashboard-lib'
import {
THEME_PREFERENCE_CHIPS,
THEME_PREFERENCES,
createNewProjectComposerState,
isNewProjectActionDisabled,
} from './dashboard-lib'

describe('dashboard theme preferences', () => {
it('exposes only explicit theme preferences instead of business templates', () => {
Expand All @@ -22,3 +27,19 @@ describe('dashboard theme preferences', () => {
})
})
})

describe('dashboard new project action', () => {
it('resets the composer to a clean draft with the default theme', () => {
expect(createNewProjectComposerState()).toEqual({
prompt: '',
inputError: null,
selectedTheme: THEME_PREFERENCE_CHIPS[0].id,
})
})

it('is disabled while projects are loading or a project is being created', () => {
expect(isNewProjectActionDisabled({ creating: false, loading: true })).toBe(true)
expect(isNewProjectActionDisabled({ creating: true, loading: false })).toBe(true)
expect(isNewProjectActionDisabled({ creating: false, loading: false })).toBe(false)
})
})
26 changes: 26 additions & 0 deletions src/features/dashboard/dashboard-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,32 @@ export const THEME_PREFERENCE_CHIPS = [

export type ThemePreferenceId = (typeof THEME_PREFERENCE_CHIPS)[number]['id']

export type NewProjectComposerState = {
prompt: string
inputError: null
selectedTheme: ThemePreferenceId
}

/** 新建项目入口始终回到干净的 Composer 草稿。 */
export function createNewProjectComposerState(): NewProjectComposerState {
return {
prompt: '',
inputError: null,
selectedTheme: THEME_PREFERENCE_CHIPS[0].id,
}
}

/** 本地项目尚未加载完成或已有创建任务时,不允许再次开始新项目。 */
export function isNewProjectActionDisabled({
creating,
loading,
}: {
creating: boolean
loading: boolean
}): boolean {
return creating || loading
}

/**
* 主题选项 → PlannerInput.themePreference 映射。当前只提供舒适密度,
* 与产品规格默认的 neutral / comfortable 保持一致;颜色预设由用户明确选择。
Expand Down
33 changes: 26 additions & 7 deletions src/features/dashboard/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import {
THEME_PREFERENCES,
THEME_PREFERENCE_CHIPS,
TOO_LONG_INPUT_MESSAGE,
createNewProjectComposerState,
deriveTitleFromPrompt,
isNewProjectActionDisabled,
type ThemePreferenceId,
} from './dashboard-lib'
import { ExamplePromptList } from './example-prompt-list'
Expand Down Expand Up @@ -55,6 +57,7 @@ export function DashboardPage() {

const textareaRef = useRef<HTMLTextAreaElement | null>(null)
const menuButtonRef = useRef<HTMLButtonElement | null>(null)
const restoreMenuFocusRef = useRef(true)

const loadProjects = useCallback(async () => {
setLoadState('loading')
Expand Down Expand Up @@ -119,13 +122,24 @@ export function DashboardPage() {
window.addEventListener('keydown', handleKeyDown)
return () => {
window.removeEventListener('keydown', handleKeyDown)
menuButtonRef.current?.focus()
if (restoreMenuFocusRef.current) {
menuButtonRef.current?.focus()
}
restoreMenuFocusRef.current = true
}
}, [drawerOpen])

const focusComposer = useCallback(() => {
const handleNewProject = useCallback(() => {
const nextComposer = createNewProjectComposerState()
setPrompt(nextComposer.prompt)
setInputError(nextComposer.inputError)
setSelectedTheme(nextComposer.selectedTheme)
window.scrollTo({ top: 0, behavior: 'smooth' })
textareaRef.current?.focus()

// 等待状态更新和移动端抽屉卸载完成,再把焦点交给 Composer。
window.requestAnimationFrame(() => {
textareaRef.current?.focus()
})
}, [])

const handlePickExample = useCallback(
Expand Down Expand Up @@ -184,6 +198,10 @@ export function DashboardPage() {
updatedAt: project.updatedAt,
phase: project.phase,
}))
const newProjectDisabled = isNewProjectActionDisabled({
creating,
loading: loadState === 'loading',
})

return (
<>
Expand Down Expand Up @@ -213,8 +231,8 @@ export function DashboardPage() {
<SidebarNavigation
projects={sidebarProjects}
loading={loadState === 'loading'}
newProjectDisabled={creating}
onNewProject={focusComposer}
newProjectDisabled={newProjectDisabled}
onNewProject={handleNewProject}
/>

{drawerOpen ? (
Expand All @@ -223,10 +241,11 @@ export function DashboardPage() {
<SidebarNavigation
projects={sidebarProjects}
loading={loadState === 'loading'}
newProjectDisabled={creating}
newProjectDisabled={newProjectDisabled}
onNewProject={() => {
restoreMenuFocusRef.current = false
setDrawerOpen(false)
focusComposer()
handleNewProject()
}}
variant="drawer"
onNavigate={() => setDrawerOpen(false)}
Expand Down