@@ -4,15 +4,15 @@
< script setup lang = "ts" >
import DOMPurify from "dompurify" ;
import { marked } from "marked" ;
import { computed , onMounted , reactive , ref , watch } from "vue" ;
import { useRouter } from "vue-router" ;
import { computed , onBeforeUnmount , onMounted , reactive , ref , watch } from "vue" ;
import { useRoute , useRouter } from "vue-router" ;
import { agentAPI , projectManagementAPI , workbenchAPI , type MdtodoFileRecord , type MdtodoReportPreviewRecord , type MdtodoTaskDetailRecord , type MdtodoTaskLinkRecord , type MdtodoTaskMutationInput , type MdtodoTaskPage , type MdtodoTaskRecord , type ProjectNavigationResponse , type ProjectSource , type ProjectSourceInput , type ProjectWorkbenchLinkRecord } from "@/api" ;
import EmptyState from "@/components/common/EmptyState.vue" ;
import LoadingState from "@/components/common/LoadingState.vue" ;
import PageHeader from "@/components/common/PageHeader.vue" ;
import type { ApiError , ErrorDiagnostic } from "@/types" ;
const router = useRouter ( ) ;
const route = useRoute ( ) ;
const loading = ref ( false ) ;
const taskLoading = ref ( false ) ;
const taskDetailLoading = ref ( false ) ;
@@ -26,6 +26,7 @@ const sourceReindexLoading = ref(false);
const showInfo = ref ( false ) ;
const showSourceConfig = ref ( false ) ;
const showReportFullscreen = ref ( false ) ;
const showTaskCreateDialog = ref ( false ) ;
const launchError = ref < string | null > ( null ) ;
const taskMutationError = ref < string | null > ( null ) ;
const taskMutationMessage = ref < string | null > ( null ) ;
@@ -48,9 +49,13 @@ const activeReportLink = ref<MdtodoTaskLinkRecord | null>(null);
const selectedSourceId = ref < string | null > ( null ) ;
const selectedFileRef = ref < string | null > ( null ) ;
const selectedTaskRef = ref < string | null > ( null ) ;
const applyingRouteSelection = ref ( false ) ;
const taskSearch = ref ( "" ) ;
const taskStatusFilter = ref ( "all" ) ;
const collapsedTaskRefs = ref < Set < string > > ( new Set ( ) ) ;
const taskPaneCollapsed = ref ( false ) ;
const taskPaneWidth = ref ( 30 ) ;
const taskPaneResize = ref < { left : number ; width : number } | null > ( null ) ;
const editingTitle = ref ( false ) ;
const editingBody = ref ( false ) ;
const editTitle = ref ( "" ) ;
@@ -122,18 +127,25 @@ const selectedTaskLinks = computed(() => taskDetail.value?.links ?? []);
const selectedTaskBodyHtml = computed ( ( ) => markdownToHtml ( selectedTaskBody . value ) ) ;
const reportPreviewHtml = computed ( ( ) => markdownToHtml ( reportPreview . value ? . content ? ? "" ) ) ;
const selectedFileName = computed ( ( ) => selectedFile . value ? fileDisplayName ( selectedFile . value ) : selectedTask . value ? . fileRef || "-" ) ;
const workspaceStyle = computed ( ( ) => ( { "--mdtodo-task-pane-width" : taskPaneCollapsed . value ? "44px" : ` ${ taskPaneWidth . value } % ` } ) ) ;
onMounted ( ( ) => void loadPage ( ) ) ;
onBeforeUnmount ( ( ) => stopTaskPaneResize ( ) ) ;
watch ( selectedSourceId , async ( sourceId ) => {
if ( ! sourceId || loading . value ) return ;
await loadFilesAndTasks ( sourceId ) ;
if ( ! sourceId || loading . value || applyingRouteSelection . value ) return ;
await loadFilesAndTasks ( sourceId , { updateRoute : true } ) ;
} ) ;
watch ( selectedTaskRef , async ( taskRef ) => {
await Promise . all ( [ loadLinks ( taskRef ) , loadTaskDetail ( taskRef ) ] ) ;
} ) ;
watch ( ( ) => route . fullPath , async ( ) => {
if ( loading . value || ! sources . value . length ) return ;
await applyRouteSelection ( ) ;
} ) ;
watch ( selectedTask , ( task ) => resetTaskEditor ( task ) , { immediate : true } ) ;
watch ( taskDetail , ( detail ) => {
@@ -151,8 +163,13 @@ async function loadPage(): Promise<void> {
if ( ! sourceResponse . ok ) throw sourceResponse ;
navigation . value = navigationResponse . data ;
sources . value = sourceResponse . data ? . sources ? ? [ ] ;
selectedSourceId . value = selectedSourceId . value && sources . value . some ( ( source ) => source . sourceId === selectedSourceId . value ) ? selectedSourceId . value : sources . value [ 0 ] ? . sourceId ? ? null ;
if ( selectedSourceId . value ) await loadFilesAndTasks ( selectedSourceId . value ) ;
const sourceFromRoute = routeParam ( "sourceId" ) ;
selectedSourceId . value = sourceFromRoute && sources . value . some ( ( source ) => source . sourceId === sourceFromRoute )
? sourceFromRoute
: selectedSourceId . value && sources . value . some ( ( source ) => source . sourceId === selectedSourceId . value )
? selectedSourceId . value
: sources . value [ 0 ] ? . sourceId ? ? null ;
if ( selectedSourceId . value ) await loadFilesAndTasks ( selectedSourceId . value , routeSelectionOptions ( true ) ) ;
else await loadLinks ( null ) ;
} catch ( err ) {
setError ( err ) ;
@@ -161,14 +178,24 @@ async function loadPage(): Promise<void> {
}
}
async function loadFilesAndTasks ( sourceId : string ) : Promise < void > {
async function loadFilesAndTasks ( sourceId : string , options : { fileRef ? : string | null ; taskId ? : string | null ; reportLinkId ? : string | null ; updateRoute ? : boolean } = { } ) : Promise < void > {
taskLoading . value = true ;
try {
const fileResponse = await projectManagementAPI . files ( sourceId ) ;
if ( ! fileResponse . ok ) throw fileResponse ;
files . value = fileResponse . data ? . files ? ? [ ] ;
selectedFileRef . value = selectedFileRef . value && files . value . some ( ( file ) => file . fileRef === selectedFileRef . value ) ? selectedFileRef . value : files . value [ 0 ] ? . fileRef ? ? null ;
await loadTaskWindow ( sourceId , selectedFileRef . value , selectedTaskRef . value ) ;
selectedFileRef . value = options . fileRef && files . value . some ( ( file ) => file . fileRef === options . fileRef )
? options . fileRef
: selectedFileRef . value && files . value . some ( ( file ) => file . fileRef === selectedFileRef . value )
? selectedFileRef . value
: files . value [ 0 ] ? . fileRef ? ? null ;
await loadTaskWindow ( sourceId , selectedFileRef . value , options . taskId ? ? selectedTaskRef . value ) ;
if ( options . reportLinkId && selectedTaskRef . value ) {
await Promise . all ( [ loadLinks ( selectedTaskRef . value ) , loadTaskDetail ( selectedTaskRef . value ) ] ) ;
await openReportPreviewById ( options . reportLinkId , false ) ;
} else if ( options . updateRoute !== false ) {
void syncSelectionRoute ( { replace : true } ) ;
}
} catch ( err ) {
setError ( err ) ;
} finally {
@@ -221,10 +248,13 @@ function onFileSelect(event: Event): void {
async function selectFile ( fileRef : string ) : Promise < void > {
selectedFileRef . value = fileRef || null ;
reportPreview . value = null ;
activeReportLink . value = null ;
if ( ! selectedSourceId . value ) return ;
taskLoading . value = true ;
try {
await loadTaskWindow ( selectedSourceId . value , selectedFileRef . value , null ) ;
void syncSelectionRoute ( { replace : false } ) ;
} catch ( err ) {
setError ( err ) ;
} finally {
@@ -234,6 +264,9 @@ async function selectFile(fileRef: string): Promise<void> {
function selectTask ( taskRef : string ) : void {
selectedTaskRef . value = taskRef ;
reportPreview . value = null ;
activeReportLink . value = null ;
void syncSelectionRoute ( { replace : false } ) ;
}
function toggleTask ( task : MdtodoTaskRecord ) : void {
@@ -247,6 +280,36 @@ function hasChildren(task: MdtodoTaskRecord): boolean {
return ( childrenByParent . value . get ( task . taskRef ) ? ? [ ] ) . length > 0 ;
}
function startTaskPaneResize ( event : PointerEvent ) : void {
if ( taskPaneCollapsed . value ) return ;
const workspace = ( event . currentTarget as HTMLElement ) . closest ( ".mdtodo-workspace" ) ;
const rect = workspace ? . getBoundingClientRect ( ) ;
if ( ! rect || rect . width <= 0 ) return ;
event . preventDefault ( ) ;
taskPaneResize . value = { left : rect . left , width : rect . width } ;
window . addEventListener ( "pointermove" , resizeTaskPane ) ;
window . addEventListener ( "pointerup" , stopTaskPaneResize , { once : true } ) ;
}
function resizeTaskPane ( event : PointerEvent ) : void {
const state = taskPaneResize . value ;
if ( ! state ) return ;
const percent = ( ( event . clientX - state . left ) / state . width ) * 100 ;
taskPaneWidth . value = Math . min ( 45 , Math . max ( 22 , Math . round ( percent ) ) ) ;
}
function stopTaskPaneResize ( ) : void {
taskPaneResize . value = null ;
window . removeEventListener ( "pointermove" , resizeTaskPane ) ;
window . removeEventListener ( "pointerup" , stopTaskPaneResize ) ;
}
function openTaskCreateDialog ( ) : void {
taskMutationError . value = null ;
taskMutationMessage . value = null ;
showTaskCreateDialog . value = true ;
}
function openSourceConfig ( ) : void {
const source = selectedSource . value ;
sourceMessage . value = null ;
@@ -362,6 +425,7 @@ async function createTask(kind: "root" | "subtask" | "continue"): Promise<void>
if ( ! taskMutationError . value ) {
newTaskTitle . value = "" ;
newTaskBody . value = "" ;
showTaskCreateDialog . value = false ;
}
}
@@ -408,8 +472,11 @@ async function openReportPreview(link: MdtodoTaskLinkRecord): Promise<void> {
try {
const response = await projectManagementAPI . reportPreview ( taskRef , link . linkId ) ;
if ( ! response . ok ) throw response ;
reportPreview . value = response . data ? . report ? ? null ;
const report = response . data ? . report ? ? null ;
if ( ! report ? . content ) throw new Error ( "报告内容未返回" ) ;
reportPreview . value = report ;
activeReportLink . value = response . data ? . link ? ? link ;
void syncSelectionRoute ( { replace : false , reportLinkId : activeReportLink . value ? . linkId ? ? link . linkId } ) ;
} catch ( err ) {
reportError . value = projectApiError ( err ) . error ;
} finally {
@@ -417,6 +484,17 @@ async function openReportPreview(link: MdtodoTaskLinkRecord): Promise<void> {
}
}
async function openReportPreviewById ( linkId : string , updateRoute = true ) : Promise < void > {
const target = selectedTaskLinks . value . find ( ( link ) => link . linkId === linkId || link . label === linkId || link . relativePath ? . includes ( linkId ) || link . href ? . includes ( linkId ) ) ;
if ( ! target ) {
reportError . value = ` 报告链接未找到: ${ linkId } ` ;
return ;
}
await openReportPreview ( target ) ;
if ( ! updateRoute ) return ;
void syncSelectionRoute ( { replace : true , reportLinkId : target . linkId } ) ;
}
async function runTaskMutation ( action : ( ) => ReturnType < typeof projectManagementAPI.updateTask > , preferredTaskRef : string | undefined , message : string ) : Promise < void > {
const sourceId = selectedSourceId . value ;
if ( ! sourceId ) return ;
@@ -449,12 +527,56 @@ async function loadTaskWindow(sourceId: string, fileRef: string | null, preferre
if ( ! taskResponse . ok ) throw taskResponse ;
tasks . value = taskResponse . data ? . tasks ? ? [ ] ;
taskPage . value = taskResponse . data ? . page ? ? null ;
const preferredTask = preferredTaskRef ? tasks . value . find ( ( task ) => task . taskRef === preferredTaskRef ) : null ;
const preferredTask = preferredTaskRef ? tasks . value . find ( ( task ) => task . taskRef === preferredTaskRef || task . taskId === preferredTaskRef || task . rxxId === preferredTaskRef ) : null ;
selectedTaskRef . value = preferredTask ? . taskRef ? ? tasks . value [ 0 ] ? . taskRef ? ? null ;
collapsedTaskRefs . value = new Set ( ) ;
if ( ! selectedTaskRef . value ) await Promise . all ( [ loadLinks ( null ) , loadTaskDetail ( null ) ] ) ;
}
async function applyRouteSelection ( ) : Promise < void > {
const sourceId = routeParam ( "sourceId" ) ;
const nextSourceId = sourceId && sources . value . some ( ( source ) => source . sourceId === sourceId ) ? sourceId : selectedSourceId . value ;
if ( ! nextSourceId ) return ;
applyingRouteSelection . value = true ;
try {
selectedSourceId . value = nextSourceId ;
await loadFilesAndTasks ( nextSourceId , routeSelectionOptions ( false ) ) ;
} finally {
applyingRouteSelection . value = false ;
}
}
function routeSelectionOptions ( updateRoute : boolean ) : { fileRef ? : string | null ; taskId ? : string | null ; reportLinkId ? : string | null ; updateRoute : boolean } {
return {
fileRef : routeParam ( "fileRef" ) ,
taskId : routeParam ( "taskId" ) ,
reportLinkId : routeParam ( "linkId" ) ,
updateRoute
} ;
}
async function syncSelectionRoute ( options : { replace ? : boolean ; reportLinkId ? : string | null } = { } ) : Promise < void > {
const sourceId = selectedSourceId . value ;
const fileRef = selectedFileRef . value ;
const task = selectedTask . value ;
if ( ! sourceId || ! fileRef ) return ;
const name = options . reportLinkId && task ? "ProjectMdtodoReport" : task ? "ProjectMdtodoTask" : "ProjectMdtodoFile" ;
const params : Record < string , string > = { sourceId , fileRef } ;
if ( task ? . taskId ) params . taskId = task . taskId ;
if ( options . reportLinkId ) params . linkId = options . reportLinkId ;
const navigation = { name , params } ;
const resolved = router . resolve ( navigation ) ;
if ( resolved . fullPath === route . fullPath ) return ;
if ( options . replace ) await router . replace ( navigation ) ;
else await router . push ( navigation ) ;
}
function routeParam ( key : string ) : string | null {
const value = route . params [ key ] ;
const text = Array . isArray ( value ) ? value [ 0 ] : value ;
return typeof text === "string" && text . trim ( ) ? text . trim ( ) : null ;
}
function taskFingerprint ( ) : string | undefined {
return selectedTask . value ? . sourceFingerprint || selectedFile . value ? . fingerprint || undefined ;
}
@@ -495,20 +617,23 @@ async function launchSelectedTask(): Promise<void> {
launchContext
} ) ;
if ( ! response . ok ) throw response ;
if ( response . data ? . sessionId ) {
const sessionId = response . data ? . sessionId ;
if ( ! sessionId ) throw new Error ( "Workbench launch 未返回 sessionId" ) ;
if ( sessionId ) {
const chat = await agentAPI . sendAgentMessage ( {
message : prompt ,
prompt ,
sessionId : response . data . sessionId ,
sessionId ,
providerProfile : "codex" ,
projectId : task . projectId ,
taskRef : task . taskRef ,
launchContext
} , 120000 ) ;
if ( ! chat . ok ) throw chat ;
await waitWorkbenchSessionHasPrompt ( sessionId ) ;
}
await loadLinks ( task . taskRef ) ;
const target = response . data ? . workbenchUrl || ( response . data ? . sessionId ? ` /workbench/sessions/ ${ encodeURIComponent ( response . data . sessionId ) } ` : null ) ;
const target = response . data ? . workbenchUrl || ` /workbench/sessions/ ${ encodeURIComponent ( sessionId ) } ` ;
if ( target ) await router . push ( target ) ;
} catch ( err ) {
launchError . value = projectApiError ( err ) . error ;
@@ -517,6 +642,23 @@ async function launchSelectedTask(): Promise<void> {
}
}
async function waitWorkbenchSessionHasPrompt ( sessionId : string ) : Promise < void > {
for ( let attempt = 0 ; attempt < 5 ; attempt += 1 ) {
const response = await workbenchAPI . session ( sessionId , 8000 ) ;
if ( response . ok ) {
const session = response . data ? . session as { messageCount ? : number ; messages ? : unknown [ ] ; firstUserMessagePreview ? : string | null } | undefined ;
const count = Number ( session ? . messageCount ? ? ( Array . isArray ( session ? . messages ) ? session . messages . length : 0 ) ) ;
if ( count > 0 || String ( session ? . firstUserMessagePreview ? ? "" ) . trim ( ) ) return ;
}
await sleep ( 800 ) ;
}
throw new Error ( "Workbench session 已创建但未投影 MDTODO prompt" ) ;
}
function sleep ( ms : number ) : Promise < void > {
return new Promise ( ( resolve ) => window . setTimeout ( resolve , ms ) ) ;
}
function setError ( err : unknown ) : void {
const context = projectApiError ( err ) ;
error . value = context . error ;
@@ -597,7 +739,13 @@ function displayDate(value?: string | null): string {
< template >
< section class = "route-stack mdtodo-page" data -testid = " project -management -mdtodo " >
< PageHeader eyebrow = "Project Management" title = "MDTODO" description = "MDTODO source、文件、任务树和 Workbench link 的项目工作区。" / >
< header class = "mdtodo-compact-header" >
< div >
< span > Project Management < / span >
< h1 > MDTODO < / h1 >
< / div >
< p > { { selectedFileName } } < small v-if = "selectedTask" > / {{ selectedTask.taskId }} < / small > < / p >
< / header >
< LoadingState v-if = "loading && !sources.length" label="加载 MDTODO source" / >
< section v-else-if = "error" class="data-panel mdtodo-error" data-testid="project-management-error" >
< EmptyState title = "MDTODO 加载失败" :description = "error" :api-error = "apiError" :diagnostic = "diagnostic" action -label = " 重试 " @action ="loadPage" / >
@@ -626,11 +774,15 @@ function displayDate(value?: string | null): string {
< p v-if = "sourceMessage" class="source-message" data-testid="mdtodo-source-message" > {{ sourceMessage }} < / p >
< p v-if = "sourceError" class="source-error" data-testid="mdtodo-source-error" > {{ sourceError }} < / p >
< section class = "mdtodo-workspace" >
< section class = "data-panel mdtodo-task-panel" data -testid = " mdtodo -task -tree " >
< section class = "mdtodo-workspace" :style = "workspaceStyle" :data-tree-collapsed = "taskPaneCollapsed" >
< section class = "data-panel mdtodo-task-panel" data -testid = " mdtodo -task -tree " :data-collapsed = "taskPaneCollapsed" >
< header class = "mdtodo-panel-header" >
< div > < h2 > 任务树 < / h2 > < p > { { filteredTasks . length } } / { { taskPage ? . total ? ? fileScopedTasks . length } } tasks < span v-if = "taskPage?.hasMore" > · window {{ taskPage.limit }} < / span > < / p > < / div >
< div class = "task-tool s" >
< div > < h2 > { { taskPaneCollapsed ? '纲' : '任务树' } } < / h2 > < p v-if = "!taskPaneCollapsed">{{ filteredTasks.length }} / {{ taskPage?.total ?? fileScopedTasks.length }} tasks<span v-if= "taskPage?.hasMore"> · window {{ taskPage.limit }} < / span > < / p > < / div >
< div class = "task-pane-action s" >
< button class = "icon-button" type = "button" data -testid = " mdtodo -new -task -open " aria -label = " 新建任务 " : disabled = "!selectedFileRef || taskMutationLoading" @click ="openTaskCreateDialog" > + < / button >
< button class = "icon-button" type = "button" data -testid = " mdtodo -task -pane -toggle " : aria -label = " taskPaneCollapsed ? ' 展开任务树 ' : ' 收起任务树 ' " @click ="taskPaneCollapsed = !taskPaneCollapsed" > {{ taskPaneCollapsed ? ' › ' : ' ‹ ' }} < / button >
< / div >
< div v-if = "!taskPaneCollapsed" class="task-tools" >
< input v-model = "taskSearch" data-testid="mdtodo-task-search" type="search" placeholder="R1 / title" aria-label="搜索任务" / >
< select v-model = "taskStatusFilter" data-testid="mdtodo-task-status-filter" aria-label="筛选状态" >
< option value = "all" > 全部 < / option >
@@ -641,6 +793,7 @@ function displayDate(value?: string | null): string {
< / select >
< / div >
< / header >
< template v-if = "!taskPaneCollapsed" >
< LoadingState v-if = "taskLoading" compact label="同步文件和任务" / >
< EmptyState v-else-if = "!visibleTaskRows.length" title="暂无任务" description="当前文件没有任务投影。" / >
< div v-else class = "task-tree" role = "tree" aria -label = " MDTODO task tree " >
@@ -653,56 +806,65 @@ function displayDate(value?: string | null): string {
< / button >
< / div >
< / div >
< / template >
< / section >
< div class = "task-pane-resizer" data -testid = " mdtodo -task -pane -resizer " role = "separator" aria -orientation = " vertical " :aria-valuenow = "taskPaneWidth" @pointerdown ="startTaskPaneResize" / >
< aside class = "data-panel mdtodo-detail-panel" data -testid = " mdtodo -task -detail " >
< header class = "mdtodo-pane l-header" > < div > < h2 > 任务详情 < / h2 > < p > { { selectedTask ? statusLabel ( selectedTask . status ) : '未选择' } } < / p > < / div > < / header >
< header class = "mdtodo-detai l-header" >
< div class = "task-title-block" >
< span class = "task-id-large" > { { selectedTask ? . taskId || 'R?' } } < / span >
< div v-if = "selectedTask && editingTitle" class="inline-editor" >
< input v-model = "editTitle" data-testid="mdtodo-edit-title" :disabled="taskMutationLoading" @keyup.enter="saveTaskBasics" @keyup.esc="cancelTitleEdit" / >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -edit -save " :disabled = "taskMutationLoading" @click ="saveTaskBasics" > 保存 < / button >
< button class = "btn btn-secondary" type = "button" :disabled = "taskMutationLoading" @click ="cancelTitleEdit" > 取消 < / button >
< / div >
< button v-else-if = "selectedTask" class="task-title-read" type="button" data-testid="mdtodo-title-read" @dblclick="beginTitleEdit" > {{ selectedTask.title | | selectedTask.taskId }} < / button >
< strong v-else > 未选择任务 < / strong >
< / div >
< div v-if = "selectedTask" class="detail-toolbar" >
< select v-model = "editStatus" data-testid="mdtodo-edit-status" :disabled="taskMutationLoading" aria-label="任务状态" >
< option value = "open" > 待办 < / option >
< option value = "in_progress" > 进行中 < / option >
< option value = "done" > 完成 < / option >
< option value = "blocked" > 阻塞 < / option >
< / select >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -status -save " :disabled = "taskMutationLoading" @click ="saveTaskBasics" > 保存状态 < / button >
< button class = "btn btn-primary" type = "button" data -testid = " mdtodo -workbench -launch " :disabled = "launchButtonDisabled" :aria-disabled = "launchButtonDisabled" @click ="launchSelectedTask" >
{ { launchLoading ? '启动中' : '在 Workbench 执行' } }
< / button >
< / div >
< / header >
< EmptyState v-if = "!selectedTask" title="未选择任务" description="选择任务后显示 taskRef、状态、文件和 Workbench link 摘要。" / >
< template v-else >
< dl class = "task-detail-lis t" >
< div >
< dt > Title < / dt >
< dd v-if = "editingTitle" class="inline-editor" >
< input v-model = "editTitle" data-testid="mdtodo-edit-title" :disabled="taskMutationLoading" @keyup.enter="saveTaskBasics" @keyup.esc="cancelTitleEdit" / >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -edit -save " :disabled = "taskMutationLoading" @click ="saveTaskBasics" > 保存 < / button >
< button class = "btn btn-secondary" type = "button" :disabled = "taskMutationLoading" @click ="cancelTitleE dit"> 取消 < / button >
< / dd >
< dd v-else class = "editable-value" data -testid = " mdtodo -title -read " @dblclick ="beginTitleEdit" > {{ selectedTask.title | | selectedTask.taskId }} < / dd >
< / div >
< div > < dt > TaskRef < / dt > < dd > < code > { { selectedTask . taskRef } } < / code > < / dd > < / div >
< div > < dt > File < / dt > < dd > < code > { { selectedFileName } } < / code > < / dd > < / div >
< div > < dt > Updated < / dt > < dd > { { displayDate ( selectedTask . updatedAt ) } } < / dd > < / div >
< div >
< dt > Status < / dt >
< dd class = "status-inline" >
< select v-model = "editStatus" data-testid="mdtodo-edit-status" :disabled="taskMutationLoading" >
< option value = "open" > 待办 < / option >
< option value = "in_progress" > 进行中 < / option >
< option value = "done" > 完成 < / option >
< option value = "blocked" > 阻塞 < / option >
< / select >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -status -save " :disabled = "taskMutationLoading" @click ="saveTaskBasics" > 保存状态 < / button >
< / dd >
< / div >
< / dl >
< section class = "task-editor" data -testid = " mdtodo -task -editor " >
< LoadingState v-if = "taskDetailLoading" compact label="同步任务详情" / >
< p v-if = "taskDetailError" class="source-error" data-testid="mdtodo-task-detail-error" > {{ taskDetailError }} < / p >
< section class = "task-body-section" data -testid = " mdtodo -task -body " >
< header > < strong > 正文 < / strong > < / header >
< div v-if = "editingBody" class="inline-body-editor" >
< textarea v-model = "editBody" data-testid="mdtodo-edit-body" :disabled="taskMutationLoading" rows="8" / >
< div class = "editor-actions" >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -edit -body -save " :disabled = "taskMutationLoading" @click ="saveTaskBody" > 保存正文 < / button >
< button class = "btn btn-secondary" type = "button" :disabled = "taskMutationLoading" @click ="cancelBodyEdit" > 取消 < / button >
< section class = "task-detail-conten t" >
< section class = "task-editor" data -testid = " mdtodo -task -editor " >
< LoadingState v-if = "taskDetailLoading" compact label="同步任务详情" / >
< p v-if = "taskDetailError" class="source-error" data-testid="mdtodo-task-detail-error" > {{ taskDetailError }} < / p >
< section class = "task-body-section" data -testid = " mdtodo -task -body " >
< header > < strong > 正文 < / strong > < small > 双击编辑 < / small > < / header >
< div v-if = "editingBody" class="inline-body-e ditor " >
< textarea v-model = "editBody" data-testid="mdtodo-edit-body" :disabled="taskMutationLoading" rows="14" / >
< div class = "editor-actions" >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -edit -body -save " :disabled = "taskMutationLoading" @click ="saveTaskBody" > 保存正文 < / button >
< button class = "btn btn-secondary" type = "button" :disabled = "taskMutationLoading" @click ="cancelBodyEdit" > 取消 < / button >
< / div >
< / div >
< article v-else class = "markdown-body task-body-rendered" data -testid = " mdtodo -body -rendered " @dblclick ="beginBodyEdit" >
< div v-if = "selectedTaskBodyHtml" v-html="selectedTaskBodyHtml" / >
< p v-else class = "empty-inline" > 暂无正文 < / p >
< / article >
< / section >
< div class = "editor-actions danger-actions" >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -delete -task " :disabled = "taskMutationLoading" @click ="deleteSelectedTask" > {{ deleteConfirm ? ' 确认删除 ' : ' 删除任务 ' }} < / button >
< button v-if = "deleteConfirm" class="btn btn-secondary" type="button" data-testid="mdtodo-delete-cancel" :disabled="taskMutationLoading" @click="deleteConfirm = false" > 取消 < / button >
< / div >
< article v-else class = "markdown-body task-body-rendered" data -testid = " mdtodo -body -rendered " @dblclick ="beginBodyEdit" >
< div v-if = "selectedTaskBodyHtml" v-html="selectedTaskBodyHtml" / >
< p v-else class = "empty-inline" > 暂无正文 < / p >
< / article >
< p v-if = "taskMutationMessage" class="source-message" data-testid="mdtodo-task-mutation-message" > {{ taskMutationMessage }} < / p >
< p v-if = "taskMutationError" class="source-error" data-testid="mdtodo-task-mutation-error" > {{ taskMutationError }} < / p >
< / section >
< section v-if = "selectedTaskLinks.length || reportPreview || reportError" class="report-section" data-testid="mdtodo-report-section" >
< aside class = "report-section" data -testid = " mdtodo -report -section " >
< header > < strong > 报告 < / strong > < span v-if = "reportLoading" > 加载中 < / span > < / header >
< div v-if = "selectedTaskLinks.length" class="report-link-list" >
< button v-for = "link in selectedTaskLinks" :key="link.linkId" class="report-link-button" type="button" data-testid="mdtodo-report-link" :disabled="link.kind !== 'markdown-report' || reportLoading" @click="openReportPreview(link)" >
@@ -710,6 +872,7 @@ function displayDate(value?: string | null): string {
< small > { { link . relativePath || link . kind } } < / small >
< / button >
< / div >
< p v-else class = "empty-inline" > 暂无报告链接 < / p >
< p v-if = "reportError" class="source-error" data-testid="mdtodo-report-error" > {{ reportError }} < / p >
< article v-if = "reportPreview" class="report-preview" data-testid="mdtodo-report-preview" >
< header >
@@ -718,26 +881,8 @@ function displayDate(value?: string | null): string {
< / header >
< div class = "markdown-body" v-html = "reportPreviewHtml" / >
< / article >
< / section >
< div class = "task-create-box" >
< label > < span > New task < / span > < input v-model = "newTaskTitle" data-testid="mdtodo-new-title" :disabled="taskMutationLoading" placeholder="新任务标题" / > < / label >
< textarea v-model = "newTaskBody" data-testid="mdtodo-new-body" :disabled="taskMutationLoading" rows="3" placeholder="新任务正文,可留空。" / >
< div class = "editor-actions" >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -add -root " :disabled = "taskMutationLoading" @click ="createTask('root')" > 新增根任务 < / button >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -add -subtask " :disabled = "taskMutationLoading" @click ="createTask('subtask')" > 新增子任务 < / button >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -continue -task " :disabled = "taskMutationLoading" @click ="createTask('continue')" > 延续同级 < / button >
< / div >
< / div >
< div class = "editor-actions danger-actions" >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -delete -task " :disabled = "taskMutationLoading" @click ="deleteSelectedTask" > {{ deleteConfirm ? ' 确认删除 ' : ' 删除任务 ' }} < / button >
< button v-if = "deleteConfirm" class="btn btn-secondary" type="button" data-testid="mdtodo-delete-cancel" :disabled="taskMutationLoading" @click="deleteConfirm = false" > 取消 < / button >
< / div >
< p v-if = "taskMutationMessage" class="source-message" data-testid="mdtodo-task-mutation-message" > {{ taskMutationMessage }} < / p >
< p v-if = "taskMutationError" class="source-error" data-testid="mdtodo-task-mutation-error" > {{ taskMutationError }} < / p >
< / aside >
< / section >
< button class = "btn btn-primary" type = "button" data -testid = " mdtodo -workbench -launch " :disabled = "launchButtonDisabled" :aria-disabled = "launchButtonDisabled" @click ="launchSelectedTask" >
{ { launchLoading ? '启动中' : '在 Workbench 执行' } }
< / button >
< p class = "launch-blocker" data -testid = " mdtodo -workbench -launch -blocker " > { { workbenchLaunchEnabled ? 'Workbench Launch API ready' : 'Workbench Launch capability unavailable' } } < / p >
< p v-if = "launchError" class="launch-blocker" data-testid="mdtodo-workbench-launch-error" > {{ launchError }} < / p >
< section class = "task-links" data -testid = " mdtodo -workbench -link -summary " >
@@ -756,14 +901,35 @@ function displayDate(value?: string | null): string {
< section class = "mdtodo-dialog mdtodo-info-dialog" >
< header > < h2 > MDTODO 摘要 < / h2 > < button class = "icon-button" type = "button" aria -label = " 关闭摘要 " @click ="showInfo = false" > × < / button > < / header >
< dl class = "summary-list" >
< div > < dt > Source < / dt > < dd > { { sources . length } } / { { selectedSource ? . displayName || '-' } } < / dd > < / div >
< div > < dt > Files < / dt > < dd > { { files . length } } / { { selectedFile ? . relativePath || '-' } } < / dd > < / div >
< div > < dt > Source < / dt > < dd > { { sources . length } } / { { selectedSource ? . displayName || selectedSourceId || '-' } } < / dd > < / div >
< div > < dt > Source ID < / dt > < dd > < code > { { selectedSourceId || '-' } } < / code > < / dd > < / div >
< div > < dt > File < / dt > < dd > { { selectedFile ? . relativePath || selectedFileName } } < / dd > < / div >
< div > < dt > File Ref < / dt > < dd > < code > { { selectedFileRef || '-' } } < / code > < / dd > < / div >
< div > < dt > Title < / dt > < dd > { { selectedTask ? . title || '-' } } < / dd > < / div >
< div > < dt > TaskRef < / dt > < dd > < code > { { selectedTask ? . taskRef || '-' } } < / code > < / dd > < / div >
< div > < dt > Updated < / dt > < dd > { { displayDate ( selectedTask ? . updatedAt ) } } < / dd > < / div >
< div > < dt > Tasks < / dt > < dd > open { { taskStatusCounts . open || 0 } } / progress { { taskStatusCounts . in _progress || 0 } } / done { { taskStatusCounts . done || 0 } } < / dd > < / div >
< div > < dt > Links < / dt > < dd > { { links . length } } / { { workbenchLaunchEnabled ? 'launch enabled' : 'launch unavailable' } } < / dd > < / div >
< / dl >
< / section >
< / div >
< div v-if = "showTaskCreateDialog" class="modal-backdrop" data-testid="mdtodo-task-create-dialog" role="dialog" aria-modal="true" aria-label="新建 MDTODO 任务" @click.self="showTaskCreateDialog = false" >
< section class = "mdtodo-dialog task-create-dialog" >
< header > < h2 > 新建任务 < / h2 > < button class = "icon-button" type = "button" aria -label = " 关闭新建任务 " @click ="showTaskCreateDialog = false" > × < / button > < / header >
< div class = "task-create-box" >
< label > < span > New task < / span > < input v-model = "newTaskTitle" data-testid="mdtodo-new-title" :disabled="taskMutationLoading" placeholder="新任务标题" / > < / label >
< textarea v-model = "newTaskBody" data-testid="mdtodo-new-body" :disabled="taskMutationLoading" rows="5" placeholder="新任务正文,可留空。" / >
< div class = "editor-actions" >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -add -root " :disabled = "taskMutationLoading" @click ="createTask('root')" > 新增根任务 < / button >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -add -subtask " : disabled = "taskMutationLoading || !selectedTask" @click ="createTask('subtask')" > 新增子任务 < / button >
< button class = "btn btn-secondary" type = "button" data -testid = " mdtodo -continue -task " : disabled = "taskMutationLoading || !selectedTask" @click ="createTask('continue')" > 延续同级 < / button >
< / div >
< p v-if = "taskMutationError" class="source-error" data-testid="mdtodo-task-mutation-error" > {{ taskMutationError }} < / p >
< / div >
< / section >
< / div >
< div v-if = "showReportFullscreen && reportPreview" class="modal-backdrop" data-testid="mdtodo-report-fullscreen-dialog" role="dialog" aria-modal="true" aria-label="报告预览" @click.self="showReportFullscreen = false" >
< section class = "mdtodo-dialog report-fullscreen-dialog" >
< header > < h2 > { { activeReportLink ? . label || reportPreview . relativePath || 'Report' } } < / h2 > < button class = "icon-button" type = "button" aria -label = " 关闭报告 " @click ="showReportFullscreen = false" > × < / button > < / header >
@@ -796,8 +962,13 @@ function displayDate(value?: string | null): string {
< / template >
< style scoped >
. mdtodo - page { min - width : 0 ; }
. mdtodo - toolbar { display : grid ; grid - template - columns : minmax ( 220 px , 0.9 fr ) minmax ( 260 px , 1.1 fr ) auto ; gap : 10 px ; align - items : end ; border : 1 px solid # d8e2df ; border - radius : 8 px ; background : # fbfdfb ; padding : 10 px ; }
. mdtodo - page { display : grid ; min - width : 0 ; gap : 10 px ; }
. mdtodo - compact - header { display : flex ; min - width : 0 ; min - height : 44 px ; align - items : center ; justify - content : space - between ; gap : 12 px ; border - bottom : 1 px solid # d8e2df ; padding : 0 2 px 8 px ; }
. mdtodo - compact - header span { color : # 64706 b ; font - size : 11 px ; font - weight : 800 ; text - transform : uppercase ; }
. mdtodo - compact - header h1 { margin : 1 px 0 0 ; color : # 14211 d ; font - size : 20 px ; line - height : 1.1 ; }
. mdtodo - compact - header p { min - width : 0 ; margin : 0 ; overflow : hidden ; color : # 52615 c ; font - size : 12 px ; font - weight : 750 ; text - overflow : ellipsis ; white - space : nowrap ; }
. mdtodo - compact - header small { color : # 0 f766e ; font : inherit ; }
. mdtodo - toolbar { display : grid ; grid - template - columns : minmax ( 220 px , 0.9 fr ) minmax ( 260 px , 1.1 fr ) auto ; gap : 10 px ; align - items : end ; border : 1 px solid # d8e2df ; border - radius : 8 px ; background : # fbfdfb ; padding : 8 px ; }
. toolbar - field { display : grid ; min - width : 0 ; gap : 5 px ; }
. toolbar - field span { color : # 52615 c ; font - size : 11 px ; font - weight : 800 ; letter - spacing : 0 ; text - transform : uppercase ; }
. toolbar - field select ,
@@ -812,13 +983,19 @@ function displayDate(value?: string | null): string {
. source - error { margin : 0 ; border - radius : 6 px ; padding : 8 px 10 px ; font - size : 13 px ; }
. source - message { border : 1 px solid # bbf7d0 ; background : # f0fdf4 ; color : # 166534 ; }
. source - error { border : 1 px solid # fecaca ; background : # fef2f2 ; color : # 991 b1b ; }
. mdtodo - workspace { display : grid ; min - height : min ( 720 px , calc ( 100 dvh - 220 px ) ) ; grid - template - columns : minmax ( 420 px , 1.15 fr ) minmax ( 340 px , 0.85 fr ) ; gap : 12 px ; }
. mdtodo - workspace { display : grid ; min - height : min ( 760 px , calc ( 100 dvh - 158 px ) ) ; grid - template - columns : minmax ( 44 px , var ( -- mdtodo - task - pane - width , 30 % ) ) 6 px minmax ( 0 , 1 fr ) ; gap : 8 px ; }
. task - pane - resizer { border - inline : 1 px solid transparent ; border - radius : 999 px ; background : linear - gradient ( 90 deg , transparent 2 px , # cbd8d2 2 px , # cbd8d2 4 px , transparent 4 px ) ; cursor : col - resize ; }
. mdtodo - workspace [ data - tree - collapsed = "true" ] { grid - template - columns : 44 px 0 minmax ( 0 , 1 fr ) ; }
. mdtodo - workspace [ data - tree - collapsed = "true" ] . task - pane - resizer { pointer - events : none ; opacity : 0 ; }
. mdtodo - task - panel ,
. mdtodo - detail - panel ,
. mdtodo - error { display : grid ; min - width : 0 ; min - height : 0 ; align - content : start ; gap : 12 px ; padding : 14 px ; overflow : auto ; }
. mdtodo - error { display : grid ; min - width : 0 ; min - height : 0 ; align - content : start ; gap : 12 px ; padding : 12 px ; overflow : auto ; }
. mdtodo - task - panel [ data - collapsed = "true" ] { justify - items : center ; padding : 8 px 4 px ; overflow : hidden ; }
. mdtodo - detail - panel { align - content : stretch ; grid - template - rows : auto minmax ( 0 , 1 fr ) auto auto ; overflow : hidden ; }
. mdtodo - panel - header { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : 12 px ; }
. mdtodo - panel - header h2 { margin : 0 ; color : # 14211 d ; font - size : 16 px ; line - height : 1.2 ; }
. mdtodo - panel - header p { margin : 3 px 0 0 ; color : # 64706 b ; font - size : 12 px ; }
. task - pane - actions { display : flex ; gap : 6 px ; margin - left : auto ; }
. task - tools { display : grid ; width : min ( 360 px , 48 % ) ; grid - template - columns : minmax ( 130 px , 1 fr ) minmax ( 110 px , 0.7 fr ) ; gap : 8 px ; }
. task - tree { display : grid ; gap : 5 px ; }
. task - row - shell { display : grid ; grid - template - columns : 24 px minmax ( 0 , 1 fr ) ; gap : 4 px ; align - items : center ; }
@@ -832,6 +1009,14 @@ function displayDate(value?: string | null): string {
. task - status [ data - status = "done" ] { border - color : # 86 efac ; background : # dcfce7 ; color : # 166534 ; }
. task - status [ data - status = "in_progress" ] { border - color : # fde68a ; background : # fffbeb ; color : # 92400 e ; }
. task - status [ data - status = "blocked" ] { border - color : # fed7aa ; background : # fff7ed ; color : # 9 a3412 ; }
. mdtodo - detail - header { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : 12 px ; border - bottom : 1 px solid # e2ece8 ; padding - bottom : 8 px ; }
. task - title - block { display : grid ; min - width : 0 ; grid - template - columns : auto minmax ( 0 , 1 fr ) ; gap : 8 px ; align - items : center ; }
. task - id - large { display : inline - flex ; min - width : 46 px ; justify - content : center ; border : 1 px solid # 99 f6e4 ; border - radius : 6 px ; background : # ecfdf5 ; color : # 0 f766e ; padding : 5 px 8 px ; font - size : 12 px ; font - weight : 850 ; }
. task - title - read { min - width : 0 ; overflow : hidden ; border : 0 ; border - radius : 6 px ; background : transparent ; color : # 14211 d ; padding : 5 px 6 px ; font : inherit ; font - size : 17 px ; font - weight : 850 ; text - align : left ; text - overflow : ellipsis ; white - space : nowrap ; }
. task - title - read : hover { background : # f4fbf8 ; }
. detail - toolbar { display : flex ; flex - wrap : wrap ; justify - content : flex - end ; gap : 8 px ; }
. detail - toolbar select { min - width : 112 px ; height : 34 px ; border : 1 px solid # cbd8d2 ; border - radius : 6 px ; background : # fff ; color : # 14211 d ; padding : 0 8 px ; font : inherit ; }
. task - detail - content { display : grid ; min - width : 0 ; min - height : 0 ; grid - template - columns : minmax ( 0 , 1 fr ) minmax ( 260 px , 0.32 fr ) ; gap : 10 px ; }
. task - detail - list ,
. summary - list { display : grid ; gap : 8 px ; margin : 0 ; }
. task - detail - list div ,
@@ -846,7 +1031,7 @@ function displayDate(value?: string | null): string {
. inline - editor { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto auto ; gap : 8 px ; align - items : center ; }
. inline - editor input { width : 100 % ; min - width : 0 ; border : 1 px solid # cbd8d4 ; border - radius : 6 px ; color : # 14211 d ; font : inherit ; padding : 8 px 9 px ; }
. status - inline { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto ; gap : 8 px ; align - items : center ; }
. task - editor { display : grid ; gap : 10 px ; border : 1 px solid # d8e2df ; border - radius : 8 px ; background : # fbfefd ; padding : 12 px ; }
. task - editor { display : grid ; min - width : 0 ; min - height : 0 ; grid - template - rows : auto minmax ( 0 , 1 fr ) auto auto auto ; gap : 10 px ; border : 1 px solid # d8e2df ; border - radius : 8 px ; background : # fbfefd ; padding : 12 px ; }
. editor - grid { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) minmax ( 132 px , 0.32 fr ) ; gap : 10 px ; }
. task - editor label { display : grid ; min - width : 0 ; gap : 5 px ; color : # 52615 c ; font - size : 12 px ; font - weight : 800 ; }
. task - editor input ,
@@ -855,10 +1040,11 @@ function displayDate(value?: string | null): string {
. task - editor textarea { min - height : 72 px ; resize : vertical ; line - height : 1.4 ; }
. editor - block { display : grid ; gap : 5 px ; }
. task - body - section ,
. report - section { display : grid ; gap : 8 px ; border - bottom : 1 px solid # e2ece8 ; padding - bottom : 10 px ; }
. report - section { display : grid ; min - width : 0 ; min - height : 0 ; gap : 8 px ; }
. task - body - section header ,
. report - section header ,
. report - preview header { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : 10 px ; color : # 14211 d ; font - size : 13 px ; }
. task - body - section header small { color : # 64706 b ; font - size : 12 px ; }
. inline - body - editor { display : grid ; gap : 8 px ; }
. markdown - body { min - width : 0 ; color : # 14211 d ; font - size : 13 px ; line - height : 1.55 ; overflow - wrap : anywhere ; }
. markdown - body : deep ( p ) ,
@@ -866,18 +1052,20 @@ function displayDate(value?: string | null): string {
. markdown - body : deep ( ol ) ,
. markdown - body : deep ( pre ) ,
. markdown - body : deep ( blockquote ) { margin : 0 0 8 px ; }
. markdown - body : deep ( pre ) { overflow : auto ; border - radius : 6 px ; background : # 0 f172a ; color : # e5e7eb ; padding : 10 px ; }
. markdown - body : deep ( pre ) { max - width : 100 % ; overflow : auto ; border : 1 px solid # d8e2df ; border - radius : 6 px ; background : # f8faf9 ; color : # 14211 d ; padding : 10 px ; white - space : pre ; }
. markdown - body : deep ( code ) { border - radius : 4 px ; background : # eef6f2 ; padding : 1 px 4 px ; color : # 0 f766e ; }
. markdown - body : deep ( pre code ) { background : transparent ; color : inherit ; padding : 0 ; white - space : inherit ; }
. markdown - body : deep ( a ) { color : # 0 f766e ; font - weight : 750 ; }
. task - body - rendered { min - height : 92 px ; max - height : 240 px ; overflow : auto ; border : 1 px solid # d8e2df ; border - radius : 6 px ; background : # fff ; padding : 10 px ; }
. task - body - rendered { min - height : min ( 520 px , calc ( 100 dvh - 330 px ) ) ; overflow : auto ; border : 1 px solid # d8e2df ; border - radius : 6 px ; background : # fff ; padding : 12 px ; }
. empty - inline { margin : 0 ; color : # 64706 b ; }
. report - section { grid - template - rows : auto auto auto minmax ( 0 , 1 fr ) ; border : 1 px solid # d8e2df ; border - radius : 8 px ; background : # fff ; padding : 10 px ; overflow : hidden ; }
. report - link - list { display : grid ; gap : 6 px ; }
. report - link - button { display : grid ; width : 100 % ; min - width : 0 ; gap : 3 px ; border : 1 px solid # d8e2df ; border - radius : 6 px ; background : # fff ; color : # 14211 d ; padding : 8 px 10 px ; text - align : left ; }
. report - link - button : disabled { opacity : 0.55 ; }
. report - link - button span { overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; font - weight : 800 ; }
. report - link - button small { color : # 64706 b ; overflow - wrap : anywhere ; }
. report - preview { display : grid ; gap : 8 px ; border : 1 px solid # d8e2df ; border - radius : 6 px ; background : # fff ; padding : 10 px ; }
. report - preview > . markdown - body { max - height : 300 px ; overflow : auto ; }
. report - preview { display : grid ; min - height : 0 ; gap : 8 px ; border - top : 1 px solid # e2ece8 ; padding - top : 8 px ; }
. report - preview > . markdown - body { min - height : 0 ; overflow : auto ; }
. task - create - box { display : grid ; gap : 8 px ; border - top : 1 px solid # e2ece8 ; padding - top : 10 px ; }
. editor - actions { display : flex ; flex - wrap : wrap ; gap : 8 px ; }
. danger - actions { border - top : 1 px solid # e2ece8 ; padding - top : 10 px ; }
@@ -892,6 +1080,8 @@ function displayDate(value?: string | null): string {
. mdtodo - dialog { display : grid ; width : min ( 720 px , calc ( 100 vw - 32 px ) ) ; gap : 14 px ; border : 1 px solid # cbd8d2 ; border - radius : 8 px ; background : # fff ; color : # 14211 d ; padding : 16 px ; box - shadow : 0 24 px 60 px rgba ( 15 , 23 , 42 , 0.22 ) ; }
. mdtodo - dialog header { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; }
. mdtodo - dialog h2 { margin : 0 ; font - size : 18 px ; }
. task - create - dialog { width : min ( 620 px , calc ( 100 vw - 32 px ) ) ; }
. task - create - dialog textarea { width : 100 % ; min - width : 0 ; border : 1 px solid # cbd8d4 ; border - radius : 6 px ; background : # fff ; color : # 14211 d ; font : inherit ; line - height : 1.4 ; padding : 8 px 9 px ; resize : vertical ; }
. report - fullscreen - dialog { width : min ( 1120 px , calc ( 100 vw - 32 px ) ) ; min - height : min ( 780 px , calc ( 100 dvh - 80 px ) ) ; align - content : start ; }
. report - fullscreen - body { max - height : calc ( 100 dvh - 170 px ) ; overflow : auto ; border : 1 px solid # e2e8f0 ; border - radius : 6 px ; padding : 14 px ; }
. source - form { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; gap : 12 px ; }
@@ -902,15 +1092,23 @@ function displayDate(value?: string | null): string {
@ media ( max - width : 980 px ) {
. mdtodo - toolbar ,
. mdtodo - workspace { grid - template - columns : 1 fr ; }
. task - pane - resizer { display : none ; }
. toolbar - actions { justify - content : flex - start ; }
. task - tools { width : 100 % ; grid - template - columns : 1 fr ; }
. mdtodo - workspace { min - height : 0 ; }
. mdtodo - task - panel [ data - collapsed = "true" ] { justify - items : stretch ; }
. task - detail - content { grid - template - columns : 1 fr ; }
. report - section { max - height : none ; }
}
@ media ( max - width : 640 px ) {
. source - form { grid - template - columns : 1 fr ; }
. editor - grid { grid - template - columns : 1 fr ; }
. inline - editor ,
. status - inline { grid - template - columns : 1 fr ; }
. mdtodo - compact - header ,
. mdtodo - detail - header { align - items : stretch ; flex - direction : column ; }
. detail - toolbar { justify - content : flex - start ; }
. task - title - block { grid - template - columns : 1 fr ; }
. task - row { grid - template - columns : minmax ( 48 px , auto ) minmax ( 0 , 1 fr ) ; }
. task - status { grid - column : 1 / - 1 ; }
}