fix(web): make MDTODO rewrite visible as bounded workspace

Issue #2216 requires a visible SPEC rewrite, not only component decomposition. Replace the card-like MDTODO shell with a compact framed workspace, pane separators, a true outline rail, a primary document pane, and a right report pane. Keep public API and web-probe testids intact.
This commit is contained in:
root
2026-06-26 20:38:45 +00:00
parent ba9c310417
commit 7ff887ff20
7 changed files with 131 additions and 95 deletions
@@ -142,6 +142,7 @@ function clamp(value: number, min: number, max: number): number {
height: 100%;
overflow: hidden;
gap: 0;
background: #fff;
}
.split-workspace-pane,
@@ -151,11 +152,24 @@ function clamp(value: number, min: number, max: number): number {
overflow: hidden;
}
.split-workspace-left {
border-right: 1px solid #d8e1e7;
background: #f8fafb;
}
.split-workspace-main {
background: #fff;
}
.split-workspace-right {
border-left: 1px solid #d8e1e7;
background: #f8fafb;
}
.split-workspace-resizer {
min-width: 0;
border-inline: 1px solid transparent;
border-radius: 999px;
background: linear-gradient(90deg, transparent 1px, #cbd8d2 2px, #cbd8d2 4px, transparent 5px);
background: linear-gradient(90deg, transparent 2px, #cbd6dd 3px, transparent 4px);
cursor: col-resize;
}
@@ -23,7 +23,7 @@ const emit = defineEmits<{
<div v-if="show" class="modal-backdrop" data-testid="mdtodo-task-create-dialog" role="dialog" aria-modal="true" aria-label="新建 MDTODO 任务" @click.self="emit('close')">
<section class="mdtodo-dialog task-create-dialog">
<header><h2>新建任务</h2><button class="icon-button" type="button" aria-label="关闭新建任务" @click="emit('close')">×</button></header>
<div class="task-create-box">
<div class="task-create-form">
<label><span>New task</span><input :value="newTitle" data-testid="mdtodo-new-title" :disabled="loading" placeholder="新任务标题" @input="emit('update:newTitle', ($event.target as HTMLInputElement).value)" /></label>
<textarea :value="newBody" data-testid="mdtodo-new-body" :disabled="loading" rows="5" placeholder="新任务正文,可留空。" @input="emit('update:newBody', ($event.target as HTMLTextAreaElement).value)" />
<div class="editor-actions">
@@ -44,11 +44,10 @@ const emit = defineEmits<{
.mdtodo-dialog h2 { margin: 0; font-size: 18px; }
.icon-button { display: inline-grid; width: 36px; height: 36px; place-items: center; border: 1px solid #cbd8d2; border-radius: 6px; background: #fff; color: #14211d; font-weight: 850; }
.task-create-dialog { width: min(620px, calc(100vw - 32px)); }
.task-create-box { display: grid; gap: 8px; border-top: 1px solid #e2ece8; padding-top: 10px; }
.task-create-box label { display: grid; min-width: 0; gap: 5px; color: #52615c; font-size: 12px; font-weight: 800; }
.task-create-box input, .task-create-box textarea { width: 100%; min-width: 0; border: 1px solid #cbd8d4; border-radius: 6px; background: #fff; color: #14211d; font: inherit; line-height: 1.4; padding: 8px 9px; }
.task-create-box textarea { resize: vertical; }
.task-create-form { display: grid; gap: 8px; border-top: 1px solid #e2ece8; padding-top: 10px; }
.task-create-form label { display: grid; min-width: 0; gap: 5px; color: #52615c; font-size: 12px; font-weight: 800; }
.task-create-form input, .task-create-form textarea { width: 100%; min-width: 0; border: 1px solid #cbd8d4; border-radius: 6px; background: #fff; color: #14211d; font: inherit; line-height: 1.4; padding: 8px 9px; }
.task-create-form textarea { resize: vertical; }
.editor-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.source-error { margin: 0; border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; border-radius: 6px; padding: 8px 10px; font-size: 13px; }
</style>
@@ -51,7 +51,7 @@ const taskSearch = ref("");
const taskStatusFilter = ref("all");
const collapsedTaskRefs = ref<Set<string>>(new Set());
const taskPaneCollapsed = ref(false);
const taskPaneWidth = ref(30);
const taskPaneWidth = ref(26);
const reportPaneCollapsed = ref(false);
const reportPaneWidth = ref(50);
@@ -368,12 +368,15 @@ function setError(err: unknown): void {
<template>
<section class="route-stack mdtodo-page" data-testid="project-management-mdtodo">
<header class="mdtodo-compact-header">
<div>
<header class="mdtodo-framebar">
<div class="mdtodo-title-lockup">
<span>Project Management</span>
<h1>MDTODO</h1>
<strong>MDTODO</strong>
</div>
<div class="mdtodo-route-chip">
<span>{{ selectedFileName }}</span>
<small v-if="selectedTask">{{ selectedTask.taskId }} · {{ selectedTask.title }}</small>
</div>
<p>{{ selectedFileName }}<small v-if="selectedTask"> / {{ selectedTask.taskId }}</small></p>
</header>
<LoadingState v-if="loading && !source.sources.value.length" label="加载 MDTODO source" />
@@ -409,6 +412,11 @@ function setError(err: unknown): void {
:left-collapsed="taskPaneCollapsed"
:right-open="reportPaneOpen"
:right-collapsed="reportPaneCollapsed"
:min-left-width="22"
:max-left-width="30"
:left-collapsed-width="42"
:min-right-width="40"
:max-right-width="62"
left-resizer-test-id="mdtodo-task-pane-resizer"
right-resizer-test-id="mdtodo-report-pane-resizer"
aria-label="MDTODO 工作区"
@@ -537,12 +545,15 @@ function setError(err: unknown): void {
</template>
<style scoped>
.mdtodo-page { display: grid; height: calc(100dvh - 68px); max-height: calc(100dvh - 68px); min-width: 0; min-height: 0; grid-template-rows: auto auto auto minmax(0, 1fr); gap: 10px; overflow: hidden; }
.mdtodo-compact-header { display: flex; min-width: 0; min-height: 40px; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #d8e2df; padding: 0 2px 6px; }
.mdtodo-compact-header span { color: #64706b; font-size: 11px; font-weight: 800; text-transform: uppercase; }
.mdtodo-compact-header h1 { margin: 1px 0 0; color: #14211d; font-size: 18px; line-height: 1.1; }
.mdtodo-compact-header p { min-width: 0; margin: 0; overflow: hidden; color: #52615c; font-size: 12px; font-weight: 750; text-overflow: ellipsis; white-space: nowrap; }
.mdtodo-compact-header small { color: #0f766e; font: inherit; }
.mdtodo-page { display: grid; height: calc(100dvh - 68px); max-height: calc(100dvh - 68px); min-width: 0; min-height: 0; grid-template-rows: 34px auto auto minmax(0, 1fr); gap: 0; overflow: hidden; border: 1px solid #cad6dd; border-radius: 8px; background: #f7fafb; }
.mdtodo-framebar { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #d8e1e7; background: #f9fbfc; padding: 0 10px; }
.mdtodo-title-lockup { display: inline-flex; min-width: 0; align-items: baseline; gap: 8px; }
.mdtodo-title-lockup span { color: #6b7a86; font-size: 10px; font-weight: 850; letter-spacing: 0; text-transform: uppercase; }
.mdtodo-title-lockup strong { color: #111827; font-size: 16px; line-height: 1; }
.mdtodo-route-chip { display: inline-flex; min-width: 0; align-items: center; justify-content: flex-end; gap: 8px; color: #52616b; font-size: 12px; font-weight: 750; }
.mdtodo-route-chip span,
.mdtodo-route-chip small { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mdtodo-route-chip small { max-width: min(46vw, 720px); color: #0f766e; font: inherit; }
.source-message, .source-error { margin: 0; border-radius: 6px; padding: 6px 10px; font-size: 13px; }
.source-message { border: 1px solid #bbf7d0; background: #f0fdf4; color: #166534; }
.source-error { border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; }
@@ -550,6 +561,7 @@ function setError(err: unknown): void {
.mdtodo-error { display: grid; min-width: 0; min-height: 0; height: 100%; align-content: start; gap: 12px; padding: 12px; overflow: auto; }
@media (max-width: 720px) {
.mdtodo-page { height: calc(100dvh - 68px); max-height: calc(100dvh - 68px); overflow: hidden; }
.mdtodo-framebar { align-items: start; flex-direction: column; gap: 2px; height: auto; padding: 6px 10px; }
.mdtodo-route-chip { width: 100%; justify-content: flex-start; }
}
</style>
@@ -26,7 +26,7 @@ const emit = defineEmits<{
</script>
<template>
<aside class="data-panel report-sidebar" data-testid="mdtodo-report-sidebar" :data-collapsed="collapsed">
<aside class="report-sidebar" data-testid="mdtodo-report-sidebar" :data-collapsed="collapsed">
<header class="report-sidebar-header">
<button class="icon-button" type="button" data-testid="mdtodo-report-sidebar-toggle" :aria-label="collapsed ? '展开报告侧栏' : '收起报告侧栏'" @click="emit('update:collapsed', !collapsed)">{{ collapsed ? '' : '' }}</button>
<strong v-if="!collapsed" class="report-title">{{ activeLink?.label || report?.relativePath || 'Report' }}</strong>
@@ -51,26 +51,25 @@ const emit = defineEmits<{
</template>
<style scoped>
.data-panel { display: grid; min-width: 0; min-height: 0; height: 100%; grid-template-rows: auto minmax(0, 1fr); border: 1px solid #d8e2df; border-radius: 8px; background: #fff; padding: 10px; overflow: hidden; }
.report-sidebar { display: grid; min-width: 0; min-height: 0; height: 100%; grid-template-rows: auto minmax(0, 1fr); background: #f8fafb; overflow: hidden; }
.report-sidebar[data-collapsed="true"] { justify-items: center; padding: 8px 4px; grid-template-rows: auto; }
.report-sidebar-header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 8px; color: #14211d; font-size: 13px; }
.report-sidebar-header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 8px; border-bottom: 1px solid #d8e1e7; color: #111827; font-size: 13px; padding: 9px 10px; }
.report-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.report-sidebar-actions { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 6px; }
.report-preview { display: grid; min-height: 0; gap: 8px; border-top: 1px solid #e2ece8; padding-top: 8px; overflow: hidden; }
.report-preview { display: grid; min-height: 0; gap: 8px; overflow: hidden; padding: 10px; }
.report-preview > .markdown-body { min-height: 0; overflow: auto; }
.report-loading { color: #64706b; font-size: 13px; padding: 12px; }
.markdown-body { min-width: 0; color: #14211d; font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
.markdown-body { min-width: 0; color: #111827; font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
.markdown-body :deep(p), .markdown-body :deep(ul), .markdown-body :deep(ol), .markdown-body :deep(pre), .markdown-body :deep(blockquote) { margin: 0 0 8px; }
.markdown-body :deep(pre) { max-width: 100%; overflow: auto; border: 1px solid #d8e2df; border-radius: 6px; background: #f8faf9; color: #14211d; padding: 10px; white-space: pre; }
.markdown-body :deep(pre) { max-width: 100%; overflow: auto; border: 1px solid #d8e2df; border-radius: 6px; background: #f8faf9; color: #111827; padding: 10px; white-space: pre; }
.markdown-body :deep(code) { border-radius: 4px; background: #eef6f2; padding: 1px 4px; color: #0f766e; }
.markdown-body :deep(pre code) { background: transparent; color: inherit; padding: 0; white-space: inherit; }
.markdown-body :deep(a) { color: #0f766e; font-weight: 750; }
.source-error { margin: 0; border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; border-radius: 6px; padding: 8px 10px; font-size: 13px; }
.icon-button { display: inline-grid; width: 36px; height: 36px; place-items: center; border: 1px solid #cbd8d2; border-radius: 6px; background: #fff; color: #14211d; font-weight: 850; }
.icon-button { display: inline-grid; width: 32px; height: 32px; place-items: center; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; font-weight: 850; }
.modal-backdrop { position: fixed; z-index: 60; inset: 0; display: grid; place-items: start center; overflow: auto; background: rgba(15, 23, 42, 0.38); padding: 8dvh 16px 32px; }
.mdtodo-dialog { display: grid; width: min(1180px, calc(100vw - 48px)); height: min(820px, calc(100dvh - 64px)); max-height: calc(100dvh - 64px); grid-template-rows: auto minmax(0, 1fr); align-content: stretch; gap: 14px; border: 1px solid #cbd8d2; border-radius: 8px; background: #fff; color: #14211d; padding: 16px; box-shadow: 0 24px 60px rgba(15, 23, 42, 0.22); overflow: hidden; }
.mdtodo-dialog header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.mdtodo-dialog h2 { margin: 0; font-size: 18px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.report-fullscreen-body { min-height: 0; height: 100%; overflow: auto; border: 1px solid #e2e8f0; border-radius: 6px; padding: 14px; }
</style>
@@ -47,7 +47,7 @@ const emit = defineEmits<{
</script>
<template>
<section class="data-panel mdtodo-detail-panel" data-testid="mdtodo-task-detail">
<section class="mdtodo-detail-panel" data-testid="mdtodo-task-detail">
<header class="mdtodo-detail-header">
<div class="task-title-block">
<span class="task-id-large">{{ task?.taskId || 'R?' }}</span>
@@ -74,12 +74,20 @@ const emit = defineEmits<{
<EmptyState v-if="!task" title="未选择任务" description="从左侧任务树选择一个任务,正文将在此处显示。" />
<template v-else>
<div class="task-status-stack">
<p v-if="mutationMessage" class="source-message" data-testid="mdtodo-task-mutation-message">{{ mutationMessage }}</p>
<p v-if="mutationError" class="source-error" data-testid="mdtodo-task-mutation-error">{{ mutationError }}</p>
<p v-if="launchError" class="launch-blocker" data-testid="mdtodo-workbench-launch-error">{{ launchError }}</p>
<p v-else-if="!launchEnabled" class="launch-blocker" data-testid="mdtodo-workbench-launch-blocker">Workbench Launch capability unavailable</p>
</div>
<section class="task-detail-content" data-testid="mdtodo-task-editor">
<LoadingState v-if="detailLoading" compact label="同步任务详情" />
<p v-if="detailError" class="source-error" data-testid="mdtodo-task-detail-error">{{ detailError }}</p>
<div class="task-detail-status">
<LoadingState v-if="detailLoading" compact label="同步任务详情" />
<p v-if="detailError" class="source-error" data-testid="mdtodo-task-detail-error">{{ detailError }}</p>
</div>
<section class="task-body-section" data-testid="mdtodo-task-body">
<header><strong>正文</strong><small>双击编辑</small></header>
<div v-if="editingBody" class="inline-body-editor">
<textarea :value="editBody" data-testid="mdtodo-edit-body" :disabled="mutationLoading" rows="14" @input="emit('update:editBody', ($event.target as HTMLTextAreaElement).value)" />
<div class="editor-actions">
@@ -92,76 +100,74 @@ const emit = defineEmits<{
<p v-else class="empty-inline">暂无正文</p>
</article>
</section>
</section>
<footer class="task-document-footer">
<section class="report-link-section" data-testid="mdtodo-report-section">
<header><strong>报告</strong><span v-if="links.length">{{ links.length }}</span></header>
<strong>报告</strong>
<div v-if="links.length" class="report-link-list">
<button v-for="link in links" :key="link.linkId" class="report-link-button" type="button" data-testid="mdtodo-report-link" :disabled="link.kind !== 'markdown-report'" @click="emit('openReport', link)">
<span>{{ link.label || link.href }}</span>
<small>{{ link.relativePath || link.kind }}</small>
</button>
</div>
<p v-else class="empty-inline">暂无报告链接</p>
<span v-else class="empty-inline">暂无报告链接</span>
</section>
<div class="editor-actions danger-actions">
<button class="btn btn-secondary" type="button" data-testid="mdtodo-delete-task" :disabled="mutationLoading" @click="emit('deleteTask')">{{ deleteConfirm ? '确认删除' : '删除任务' }}</button>
<button v-if="deleteConfirm" class="btn btn-secondary" type="button" data-testid="mdtodo-delete-cancel" :disabled="mutationLoading" @click="emit('cancelDelete')">取消</button>
</div>
<p v-if="mutationMessage" class="source-message" data-testid="mdtodo-task-mutation-message">{{ mutationMessage }}</p>
<p v-if="mutationError" class="source-error" data-testid="mdtodo-task-mutation-error">{{ mutationError }}</p>
<p class="launch-blocker" data-testid="mdtodo-workbench-launch-blocker">{{ launchEnabled ? '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>
</footer>
</template>
</section>
</template>
<style scoped>
.data-panel { display: grid; min-width: 0; min-height: 0; height: 100%; align-content: stretch; grid-template-rows: auto minmax(0, 1fr); gap: 12px; padding: 12px; overflow: hidden; }
.mdtodo-detail-header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #e2ece8; padding-bottom: 8px; }
.mdtodo-detail-panel { display: grid; min-width: 0; min-height: 0; height: 100%; align-content: stretch; grid-template-rows: auto auto minmax(0, 1fr) auto; gap: 0; overflow: hidden; background: #fff; }
.mdtodo-detail-header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 10px; border-bottom: 1px solid #e2e8ed; padding: 10px 12px; }
.task-title-block { display: grid; min-width: 0; grid-template-columns: auto minmax(0, 1fr); gap: 8px; align-items: center; }
.task-id-large { display: inline-flex; min-width: 46px; justify-content: center; border: 1px solid #99f6e4; border-radius: 6px; background: #ecfdf5; color: #0f766e; padding: 5px 8px; font-size: 12px; font-weight: 850; }
.task-title-read { min-width: 0; overflow: hidden; border: 0; border-radius: 6px; background: transparent; color: #14211d; padding: 5px 6px; font: inherit; font-size: 17px; font-weight: 850; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
.task-title-read:hover { background: #f4fbf8; }
.task-id-large { display: inline-flex; min-width: 46px; justify-content: center; border: 1px solid #9be3dc; border-radius: 6px; background: #e8faf6; color: #0f766e; padding: 5px 8px; font-size: 12px; font-weight: 850; }
.task-title-read { min-width: 0; overflow: hidden; border: 0; border-radius: 5px; background: transparent; color: #111827; padding: 5px 6px; font: inherit; font-size: 18px; font-weight: 850; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
.task-title-read:hover { background: #f3f8f8; }
.inline-editor { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; align-items: center; }
.inline-editor input { width: 100%; min-width: 0; border: 1px solid #cbd8d4; border-radius: 6px; color: #14211d; font: inherit; padding: 8px 9px; }
.inline-editor input { width: 100%; min-width: 0; border: 1px solid #cbd6dd; border-radius: 6px; color: #111827; font: inherit; padding: 8px 9px; }
.detail-toolbar { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.detail-toolbar select { min-width: 112px; height: 34px; border: 1px solid #cbd8d2; border-radius: 6px; background: #fff; color: #14211d; padding: 0 8px; font: inherit; }
.task-detail-content { display: grid; min-width: 0; min-height: 0; align-content: start; grid-template-rows: auto auto minmax(0, 1fr) auto auto auto auto auto; gap: 10px; overflow: auto; }
.task-body-section { display: grid; min-width: 0; min-height: 0; gap: 8px; flex: 1 1 auto; }
.task-body-section header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 10px; color: #14211d; font-size: 13px; }
.task-body-section header small { color: #64706b; font-size: 12px; }
.task-body-rendered { min-height: 120px; overflow: auto; border: 1px solid #d8e2df; border-radius: 6px; background: #fff; padding: 14px; flex: 1 1 auto; }
.markdown-body { min-width: 0; color: #14211d; font-size: 13px; line-height: 1.6; overflow-wrap: anywhere; }
.detail-toolbar select { min-width: 108px; height: 32px; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; padding: 0 8px; font: inherit; font-size: 13px; }
.task-status-stack { display: grid; min-width: 0; gap: 6px; }
.task-detail-content { display: grid; min-width: 0; min-height: 0; grid-template-rows: auto minmax(0, 1fr); gap: 0; overflow: hidden; }
.task-detail-status { display: grid; min-width: 0; gap: 6px; }
.task-body-section { display: grid; min-width: 0; min-height: 0; }
.task-body-rendered { min-height: 0; height: 100%; overflow: auto; border: 0; background: #fff; padding: 16px 18px 20px; }
.markdown-body { min-width: 0; color: #111827; font-size: 14px; line-height: 1.65; overflow-wrap: anywhere; }
.markdown-body :deep(p), .markdown-body :deep(ul), .markdown-body :deep(ol), .markdown-body :deep(pre), .markdown-body :deep(blockquote) { margin: 0 0 10px; }
.markdown-body :deep(pre) { max-width: 100%; overflow: auto; border: 1px solid #d8e2df; border-radius: 6px; background: #f8faf9; color: #14211d; padding: 10px; white-space: pre; }
.markdown-body :deep(pre) { max-width: 100%; overflow: auto; border: 1px solid #d8e2df; border-radius: 6px; background: #f8faf9; color: #111827; padding: 10px; white-space: pre; }
.markdown-body :deep(code) { border-radius: 4px; background: #eef6f2; padding: 1px 4px; color: #0f766e; }
.markdown-body :deep(pre code) { background: transparent; color: inherit; padding: 0; white-space: inherit; }
.markdown-body :deep(a) { color: #0f766e; font-weight: 750; }
.empty-inline { margin: 0; color: #64706b; }
.inline-body-editor { display: grid; gap: 8px; }
.inline-body-editor textarea { width: 100%; min-width: 0; min-height: 240px; border: 1px solid #cbd8d4; border-radius: 6px; background: #fff; color: #14211d; font: inherit; line-height: 1.5; padding: 10px; resize: vertical; }
.report-link-section { display: grid; min-height: 0; gap: 8px; border: 1px solid #d8e2df; border-radius: 8px; background: #fff; padding: 10px; }
.report-link-section header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 10px; color: #14211d; font-size: 13px; }
.report-link-section header span { color: #64706b; font-size: 12px; }
.report-link-list { display: grid; gap: 6px; }
.report-link-button { display: grid; width: 100%; min-width: 0; gap: 3px; border: 1px solid #d8e2df; border-radius: 6px; background: #fff; color: #14211d; padding: 8px 10px; text-align: left; }
.inline-body-editor { display: grid; min-height: 0; height: 100%; grid-template-rows: minmax(0, 1fr) auto; gap: 8px; padding: 12px; }
.inline-body-editor textarea { width: 100%; min-width: 0; min-height: 0; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; font: inherit; line-height: 1.5; padding: 10px; resize: none; }
.task-document-footer { display: grid; min-width: 0; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: end; border-top: 1px solid #e2e8ed; background: #fbfcfd; padding: 9px 12px; }
.report-link-section { display: grid; min-width: 0; min-height: 0; grid-template-columns: auto minmax(0, 1fr); gap: 8px; align-items: center; }
.report-link-section > strong { color: #5d6a73; font-size: 11px; font-weight: 850; text-transform: uppercase; }
.report-link-list { display: flex; min-width: 0; gap: 6px; overflow-x: auto; padding-bottom: 1px; }
.report-link-button { display: grid; flex: 0 0 min(280px, 40vw); min-width: 0; gap: 2px; border: 1px solid #d8e2df; border-radius: 6px; background: #fff; color: #111827; padding: 6px 8px; 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: #64706b; overflow-wrap: anywhere; }
.editor-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.danger-actions { border-top: 1px solid #e2ece8; padding-top: 10px; }
.danger-actions { justify-content: flex-end; }
.danger-actions .btn { border-color: #fecaca; color: #991b1b; }
.source-message { margin: 0; border: 1px solid #bbf7d0; background: #f0fdf4; color: #166534; border-radius: 6px; padding: 8px 10px; font-size: 13px; }
.source-error { margin: 0; border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; border-radius: 6px; padding: 8px 10px; font-size: 13px; }
.launch-blocker { margin: 0; color: #64706b; font-size: 12px; }
.launch-blocker { margin: 0; border-bottom: 1px solid #e2e8ed; background: #fff7ed; color: #9a3412; padding: 7px 12px; font-size: 12px; }
@media (max-width: 640px) {
.mdtodo-detail-header { flex-direction: column; align-items: stretch; }
.detail-toolbar { justify-content: flex-start; }
.task-title-block { grid-template-columns: 1fr; }
.inline-editor { grid-template-columns: 1fr; }
.task-document-footer { grid-template-columns: 1fr; }
.report-link-section { grid-template-columns: 1fr; }
}
</style>
@@ -42,14 +42,19 @@ function toggleTask(task: MdtodoTaskRecord): void {
</script>
<template>
<section class="data-panel mdtodo-task-panel" data-testid="mdtodo-task-tree" :data-collapsed="collapsed">
<section class="mdtodo-task-panel" data-testid="mdtodo-task-tree" :data-collapsed="collapsed">
<header class="mdtodo-panel-header">
<div><h2>{{ collapsed ? '纲' : '任务树' }}</h2><p v-if="!collapsed">{{ tree.visibleTaskRows.value.length }} / {{ taskPage?.total ?? fileScopedTaskCount }} tasks<span v-if="taskPage?.hasMore"> · window {{ taskPage.limit }}</span></p></div>
<div class="task-tree-title">
<h2>{{ collapsed ? '纲' : '任务树' }}</h2>
<p v-if="!collapsed">{{ tree.visibleTaskRows.value.length }} / {{ taskPage?.total ?? fileScopedTaskCount }} tasks<span v-if="taskPage?.hasMore"> · window {{ taskPage.limit }}</span></p>
</div>
<div class="task-pane-actions">
<button class="icon-button" type="button" data-testid="mdtodo-new-task-open" aria-label="新建任务" @click="emit('openCreateTask')">+</button>
<button class="icon-button" type="button" data-testid="mdtodo-task-pane-toggle" :aria-label="collapsed ? '展开任务树' : '收起任务树'" @click="emit('update:collapsed', !collapsed)">{{ collapsed ? '' : '' }}</button>
</div>
<div v-if="!collapsed" class="task-tools">
</header>
<template v-if="!collapsed">
<div class="task-tools">
<input :value="search" data-testid="mdtodo-task-search" type="search" placeholder="R1 / title" aria-label="搜索任务" @input="emit('update:search', ($event.target as HTMLInputElement).value)" />
<select :value="statusFilter" data-testid="mdtodo-task-status-filter" aria-label="筛选状态" @change="emit('update:statusFilter', ($event.target as HTMLSelectElement).value)">
<option value="all">全部</option>
@@ -59,8 +64,6 @@ function toggleTask(task: MdtodoTaskRecord): void {
<option value="blocked">阻塞</option>
</select>
</div>
</header>
<template v-if="!collapsed">
<LoadingState v-if="loading" compact label="同步文件和任务" />
<EmptyState v-else-if="!tree.visibleTaskRows.value.length" title="暂无任务" description="当前文件没有任务投影。" />
<div v-else class="task-tree" role="tree" aria-label="MDTODO task tree">
@@ -78,27 +81,29 @@ function toggleTask(task: MdtodoTaskRecord): void {
</template>
<style scoped>
.data-panel { display: grid; min-width: 0; min-height: 0; height: 100%; align-content: start; gap: 12px; padding: 12px; overflow: hidden; grid-template-rows: auto auto minmax(0, 1fr); }
.mdtodo-task-panel[data-collapsed="true"] { justify-items: center; padding: 8px 4px; overflow: hidden; grid-template-rows: auto auto; }
.mdtodo-panel-header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 12px; }
.mdtodo-panel-header h2 { margin: 0; color: #14211d; font-size: 16px; line-height: 1.2; }
.mdtodo-panel-header p { margin: 3px 0 0; color: #64706b; font-size: 12px; }
.mdtodo-task-panel { display: grid; min-width: 0; min-height: 0; height: 100%; align-content: stretch; grid-template-rows: auto auto minmax(0, 1fr); gap: 8px; overflow: hidden; padding: 10px 8px 10px 10px; }
.mdtodo-task-panel[data-collapsed="true"] { justify-items: center; padding: 8px 4px; overflow: hidden; grid-template-rows: auto; }
.mdtodo-panel-header { display: grid; min-width: 0; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; }
.task-tree-title { min-width: 0; }
.mdtodo-panel-header h2 { min-width: 0; margin: 0; overflow: hidden; color: #111827; font-size: 14px; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; }
.mdtodo-panel-header p { margin: 2px 0 0; color: #64706b; font-size: 11px; }
.task-pane-actions { display: flex; gap: 6px; margin-left: auto; }
.task-tools { display: grid; width: min(360px, 100%); grid-template-columns: minmax(130px, 1fr) minmax(110px, 0.7fr); gap: 8px; }
.task-tools input, .task-tools select { width: 100%; min-width: 0; height: 36px; border: 1px solid #cbd8d2; border-radius: 6px; background: #fff; color: #14211d; padding: 0 10px; font: inherit; }
.task-tree { display: grid; min-height: 0; gap: 5px; overflow: auto; }
.task-tools { display: grid; min-width: 0; grid-template-columns: minmax(0, 1fr) minmax(96px, 0.55fr); gap: 6px; }
.task-tools input, .task-tools select { width: 100%; min-width: 0; height: 30px; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; padding: 0 8px; font: inherit; font-size: 12px; }
.task-tree { display: grid; min-height: 0; align-content: start; gap: 1px; overflow: auto; border-top: 1px solid #e3e9ee; padding-top: 2px; }
.task-row-shell { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 4px; align-items: center; }
.task-toggle { width: 24px; height: 24px; border: 1px solid #d8e2df; border-radius: 4px; background: #fff; color: #31423c; font-weight: 850; line-height: 1; }
.task-toggle { width: 24px; height: 24px; border: 1px solid transparent; border-radius: 4px; background: transparent; color: #52616b; font-weight: 850; line-height: 1; }
.task-toggle:hover:not(:disabled) { border-color: #cbd6dd; background: #fff; }
.task-toggle:disabled { border-color: transparent; background: transparent; }
.task-row { display: grid; width: 100%; min-width: 0; grid-template-columns: minmax(52px, 0.22fr) minmax(0, 1fr) auto; gap: 8px; align-items: center; border: 1px solid #d8e2df; border-radius: 6px; background: #fff; color: #14211d; padding: 8px 10px; text-align: left; }
.task-row[data-selected="true"] { border-color: #0f766e; background: #ecfdf5; box-shadow: inset 3px 0 0 #0f766e; }
.task-row { display: grid; width: 100%; min-width: 0; grid-template-columns: minmax(44px, 0.2fr) minmax(0, 1fr) auto; gap: 7px; align-items: center; border: 1px solid transparent; border-radius: 5px; background: transparent; color: #111827; padding: 7px 8px; text-align: left; }
.task-row:hover { border-color: #d9e2e8; background: #fff; }
.task-row[data-selected="true"] { border-color: #5bb7af; background: #e8faf6; box-shadow: inset 3px 0 0 #0f766e; }
.task-id { color: #0f766e; font-size: 12px; font-weight: 850; }
.task-row strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-status { display: inline-flex; width: fit-content; max-width: 100%; align-items: center; border-radius: 999px; border: 1px solid #bae6fd; background: #f0f9ff; color: #075985; padding: 4px 8px; font-size: 11px; font-weight: 750; line-height: 1; }
.task-status { display: inline-flex; width: fit-content; max-width: 100%; align-items: center; border-radius: 999px; border: 1px solid #bae6fd; background: #f0f9ff; color: #075985; padding: 3px 7px; font-size: 10px; font-weight: 750; line-height: 1; }
.task-status[data-status="done"] { border-color: #86efac; background: #dcfce7; color: #166534; }
.task-status[data-status="in_progress"] { border-color: #fde68a; background: #fffbeb; color: #92400e; }
.task-status[data-status="blocked"] { border-color: #fed7aa; background: #fff7ed; color: #9a3412; }
.icon-button { display: inline-grid; width: 36px; height: 36px; place-items: center; border: 1px solid #cbd8d2; border-radius: 6px; background: #fff; color: #14211d; font-weight: 850; }
.icon-button { display: inline-grid; width: 30px; height: 30px; place-items: center; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; font-weight: 850; }
.icon-button:disabled { opacity: 0.5; }
</style>
@@ -47,12 +47,12 @@ function onFileSelect(event: Event): void {
<option v-for="file in files" :key="file.fileRef" :value="file.fileRef">{{ fileDisplayName(file) }}</option>
</select>
</label>
<div class="toolbar-context">
<div class="toolbar-context" data-testid="mdtodo-toolbar-context">
<span class="toolbar-file-name" :title="selectedFileName">{{ selectedFileName }}</span>
<small v-if="selectedTaskLabel" class="toolbar-task-label">{{ selectedTaskLabel }}</small>
</div>
<div class="toolbar-actions">
<button class="btn btn-secondary" type="button" data-testid="mdtodo-new-task-open" @click="emit('openCreateTask')">+ 任务</button>
<button class="btn btn-secondary toolbar-create" type="button" data-testid="mdtodo-new-task-open" @click="emit('openCreateTask')">+ 任务</button>
<button class="icon-button" type="button" data-testid="mdtodo-source-config-open" aria-label="配置 Source" @click="emit('openSourceConfig')"></button>
<button class="icon-button" type="button" data-testid="mdtodo-source-reindex" aria-label="重建索引" :disabled="!selectedSourceId || reindexLoading" @click="emit('reindex')"></button>
<button class="icon-button" type="button" data-testid="mdtodo-info-open" aria-label="查看摘要" @click="emit('openInfo')"></button>
@@ -61,15 +61,16 @@ function onFileSelect(event: Event): void {
</template>
<style scoped>
.mdtodo-toolbar { display: grid; grid-template-columns: minmax(180px, 0.8fr) minmax(220px, 1fr) minmax(0, 1fr) auto; gap: 10px; align-items: end; border: 1px solid #d8e2df; border-radius: 8px; background: #fbfdfb; padding: 8px; }
.toolbar-field { display: grid; min-width: 0; gap: 5px; }
.toolbar-field span { color: #52615c; font-size: 11px; font-weight: 800; text-transform: uppercase; }
.toolbar-field select { width: 100%; min-width: 0; height: 36px; border: 1px solid #cbd8d2; border-radius: 6px; background: #fff; color: #14211d; padding: 0 10px; font: inherit; }
.toolbar-context { display: grid; min-width: 0; gap: 2px; align-content: center; padding: 0 4px; }
.toolbar-file-name { min-width: 0; overflow: hidden; color: #14211d; font-size: 12px; font-weight: 750; text-overflow: ellipsis; white-space: nowrap; }
.toolbar-task-label { color: #0f766e; font-size: 11px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.toolbar-actions { display: flex; gap: 8px; justify-content: flex-end; align-items: center; }
.icon-button { display: inline-grid; width: 36px; height: 36px; place-items: center; border: 1px solid #cbd8d2; border-radius: 6px; background: #fff; color: #14211d; font-weight: 850; }
.mdtodo-toolbar { display: grid; grid-template-columns: minmax(160px, 240px) minmax(220px, 360px) minmax(0, 1fr) auto; gap: 8px; align-items: center; border-bottom: 1px solid #d8e1e7; background: #ffffff; padding: 7px 10px; }
.toolbar-field { display: grid; min-width: 0; grid-template-columns: auto minmax(0, 1fr); gap: 7px; align-items: center; }
.toolbar-field span { color: #5d6a73; font-size: 10px; font-weight: 850; letter-spacing: 0; text-transform: uppercase; }
.toolbar-field select { width: 100%; min-width: 0; height: 32px; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; padding: 0 9px; font: inherit; font-size: 13px; }
.toolbar-context { display: grid; min-width: 0; gap: 1px; align-content: center; border-left: 1px solid #e2e8ed; padding-left: 10px; }
.toolbar-file-name { min-width: 0; overflow: hidden; color: #111827; font-size: 12px; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }
.toolbar-task-label { color: #0f766e; font-size: 11px; font-weight: 750; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.toolbar-actions { display: flex; gap: 6px; justify-content: flex-end; align-items: center; }
.toolbar-create { min-height: 32px; border-color: #a7c2c0; color: #0f4f4a; }
.icon-button { display: inline-grid; width: 32px; height: 32px; place-items: center; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; font-weight: 850; }
.icon-button:disabled { opacity: 0.5; }
@media (max-width: 980px) {
.mdtodo-toolbar { grid-template-columns: 1fr 1fr auto; }
@@ -77,6 +78,6 @@ function onFileSelect(event: Event): void {
}
@media (max-width: 640px) {
.mdtodo-toolbar { grid-template-columns: 1fr; }
.toolbar-field { grid-template-columns: 1fr; gap: 4px; }
}
</style>