fix(web): stabilize mdtodo split workspace layout
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<!-- Responsibility: Generic bounded split workspace layout for tool pages. -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref } from "vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
leftCollapsed?: boolean;
|
||||
rightOpen?: boolean;
|
||||
rightCollapsed?: boolean;
|
||||
leftWidth?: number;
|
||||
rightWidth?: number;
|
||||
minLeftWidth?: number;
|
||||
maxLeftWidth?: number;
|
||||
minRightWidth?: number;
|
||||
maxRightWidth?: number;
|
||||
leftCollapsedWidth?: number;
|
||||
rightCollapsedWidth?: number;
|
||||
leftResizerTestId?: string;
|
||||
rightResizerTestId?: string;
|
||||
ariaLabel?: string;
|
||||
}>(), {
|
||||
leftCollapsed: false,
|
||||
rightOpen: false,
|
||||
rightCollapsed: false,
|
||||
leftWidth: 30,
|
||||
rightWidth: 50,
|
||||
minLeftWidth: 18,
|
||||
maxLeftWidth: 36,
|
||||
minRightWidth: 30,
|
||||
maxRightWidth: 70,
|
||||
leftCollapsedWidth: 44,
|
||||
rightCollapsedWidth: 44,
|
||||
leftResizerTestId: undefined,
|
||||
rightResizerTestId: undefined,
|
||||
ariaLabel: "工作区布局"
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:leftWidth": [value: number];
|
||||
"update:rightWidth": [value: number];
|
||||
}>();
|
||||
|
||||
const layoutRef = ref<HTMLElement | null>(null);
|
||||
const resizing = ref<"left" | "right" | null>(null);
|
||||
|
||||
const clampedLeftWidth = computed(() => clamp(props.leftWidth, props.minLeftWidth, props.maxLeftWidth));
|
||||
const clampedRightWidth = computed(() => clamp(props.rightWidth, props.minRightWidth, props.maxRightWidth));
|
||||
|
||||
const layoutStyle = computed(() => {
|
||||
const leftColumn = props.leftCollapsed
|
||||
? `${props.leftCollapsedWidth}px`
|
||||
: `minmax(${props.leftCollapsedWidth}px, ${clampedLeftWidth.value}%)`;
|
||||
if (!props.rightOpen) {
|
||||
return { gridTemplateColumns: `${leftColumn} 6px minmax(0, 1fr)` };
|
||||
}
|
||||
const mainShare = props.rightCollapsed ? "minmax(0, 1fr)" : `minmax(0, ${100 - clampedRightWidth.value}fr)`;
|
||||
const rightResizeColumn = props.rightCollapsed ? "0" : "6px";
|
||||
const rightColumn = props.rightCollapsed ? `${props.rightCollapsedWidth}px` : `minmax(280px, ${clampedRightWidth.value}fr)`;
|
||||
return { gridTemplateColumns: `${leftColumn} 6px ${mainShare} ${rightResizeColumn} ${rightColumn}` };
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => stopResize());
|
||||
|
||||
function startResize(kind: "left" | "right", event: PointerEvent): void {
|
||||
if (kind === "left" && props.leftCollapsed) return;
|
||||
if (kind === "right" && (!props.rightOpen || props.rightCollapsed)) return;
|
||||
event.preventDefault();
|
||||
resizing.value = kind;
|
||||
window.addEventListener("pointermove", resizePane);
|
||||
window.addEventListener("pointerup", stopResize, { once: true });
|
||||
}
|
||||
|
||||
function resizePane(event: PointerEvent): void {
|
||||
const rect = layoutRef.value?.getBoundingClientRect();
|
||||
if (!rect || rect.width <= 0 || !resizing.value) return;
|
||||
if (resizing.value === "left") {
|
||||
emit("update:leftWidth", Math.round(clamp(((event.clientX - rect.left) / rect.width) * 100, props.minLeftWidth, props.maxLeftWidth)));
|
||||
return;
|
||||
}
|
||||
const leftPixels = props.leftCollapsed ? props.leftCollapsedWidth : rect.width * (clampedLeftWidth.value / 100);
|
||||
const remaining = Math.max(320, rect.width - leftPixels - 18);
|
||||
emit("update:rightWidth", Math.round(clamp(((rect.right - event.clientX) / remaining) * 100, props.minRightWidth, props.maxRightWidth)));
|
||||
}
|
||||
|
||||
function stopResize(): void {
|
||||
resizing.value = null;
|
||||
window.removeEventListener("pointermove", resizePane);
|
||||
window.removeEventListener("pointerup", stopResize);
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
if (!Number.isFinite(value)) return min;
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
ref="layoutRef"
|
||||
class="split-workspace-layout"
|
||||
:style="layoutStyle"
|
||||
:aria-label="ariaLabel"
|
||||
:data-left-collapsed="leftCollapsed"
|
||||
:data-right-open="rightOpen"
|
||||
:data-right-collapsed="rightCollapsed"
|
||||
:data-resizing="resizing || undefined"
|
||||
>
|
||||
<aside class="split-workspace-pane split-workspace-left" :data-collapsed="leftCollapsed">
|
||||
<slot name="left" />
|
||||
</aside>
|
||||
<div
|
||||
class="split-workspace-resizer split-workspace-resizer-left"
|
||||
:data-testid="leftResizerTestId"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
:aria-valuenow="Math.round(clampedLeftWidth)"
|
||||
@pointerdown="startResize('left', $event)"
|
||||
/>
|
||||
<main class="split-workspace-main">
|
||||
<slot />
|
||||
</main>
|
||||
<div
|
||||
v-if="rightOpen"
|
||||
class="split-workspace-resizer split-workspace-resizer-right"
|
||||
:data-testid="rightResizerTestId"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
:aria-valuenow="Math.round(clampedRightWidth)"
|
||||
@pointerdown="startResize('right', $event)"
|
||||
/>
|
||||
<aside v-if="rightOpen" class="split-workspace-pane split-workspace-right" :data-collapsed="rightCollapsed">
|
||||
<slot name="right" />
|
||||
</aside>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.split-workspace-layout {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.split-workspace-pane,
|
||||
.split-workspace-main {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.split-workspace-resizer-right {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.split-workspace-layout[data-left-collapsed="true"] .split-workspace-resizer-left,
|
||||
.split-workspace-layout[data-right-collapsed="true"] .split-workspace-resizer-right {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.split-workspace-layout[data-resizing] {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.split-workspace-layout {
|
||||
display: grid;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 10px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.split-workspace-resizer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,11 +4,12 @@
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "dompurify";
|
||||
import { marked } from "marked";
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { computed, 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 SplitWorkspaceLayout from "@/components/layout/SplitWorkspaceLayout.vue";
|
||||
import type { ApiError, ErrorDiagnostic } from "@/types";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -55,7 +56,8 @@ 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 reportPaneCollapsed = ref(false);
|
||||
const reportPaneWidth = ref(50);
|
||||
const editingTitle = ref(false);
|
||||
const editingBody = ref(false);
|
||||
const editTitle = ref("");
|
||||
@@ -127,10 +129,9 @@ 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}%` }));
|
||||
const reportPaneOpen = computed(() => Boolean(reportPreview.value));
|
||||
|
||||
onMounted(() => void loadPage());
|
||||
onBeforeUnmount(() => stopTaskPaneResize());
|
||||
|
||||
watch(selectedSourceId, async (sourceId) => {
|
||||
if (!sourceId || loading.value || applyingRouteSelection.value) return;
|
||||
@@ -280,30 +281,6 @@ 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;
|
||||
@@ -475,6 +452,7 @@ async function openReportPreview(link: MdtodoTaskLinkRecord): Promise<void> {
|
||||
const report = response.data?.report ?? null;
|
||||
if (!report?.content) throw new Error("报告内容未返回");
|
||||
reportPreview.value = report;
|
||||
reportPaneCollapsed.value = false;
|
||||
activeReportLink.value = response.data?.link ?? link;
|
||||
void syncSelectionRoute({ replace: false, reportLinkId: activeReportLink.value?.linkId ?? link.linkId });
|
||||
} catch (err) {
|
||||
@@ -484,6 +462,14 @@ async function openReportPreview(link: MdtodoTaskLinkRecord): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function closeReportPreview(updateRoute = true): void {
|
||||
reportPreview.value = null;
|
||||
activeReportLink.value = null;
|
||||
showReportFullscreen.value = false;
|
||||
reportPaneCollapsed.value = false;
|
||||
if (updateRoute) void syncSelectionRoute({ replace: false });
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -774,7 +760,18 @@ 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" :style="workspaceStyle" :data-tree-collapsed="taskPaneCollapsed">
|
||||
<SplitWorkspaceLayout
|
||||
v-model:left-width="taskPaneWidth"
|
||||
v-model:right-width="reportPaneWidth"
|
||||
class="mdtodo-workspace"
|
||||
:left-collapsed="taskPaneCollapsed"
|
||||
:right-open="reportPaneOpen"
|
||||
:right-collapsed="reportPaneCollapsed"
|
||||
left-resizer-test-id="mdtodo-task-pane-resizer"
|
||||
right-resizer-test-id="mdtodo-report-pane-resizer"
|
||||
aria-label="MDTODO 工作区"
|
||||
>
|
||||
<template #left>
|
||||
<section class="data-panel mdtodo-task-panel" data-testid="mdtodo-task-tree" :data-collapsed="taskPaneCollapsed">
|
||||
<header class="mdtodo-panel-header">
|
||||
<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>
|
||||
@@ -808,10 +805,9 @@ function displayDate(value?: string | null): string {
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<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">
|
||||
<section class="data-panel mdtodo-detail-panel" data-testid="mdtodo-task-detail">
|
||||
<header class="mdtodo-detail-header">
|
||||
<div class="task-title-block">
|
||||
<span class="task-id-large">{{ selectedTask?.taskId || 'R?' }}</span>
|
||||
@@ -864,7 +860,7 @@ function displayDate(value?: string | null): string {
|
||||
<p v-if="taskMutationError" class="source-error" data-testid="mdtodo-task-mutation-error">{{ taskMutationError }}</p>
|
||||
</section>
|
||||
|
||||
<aside class="report-section" data-testid="mdtodo-report-section">
|
||||
<section class="report-link-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)">
|
||||
@@ -874,14 +870,7 @@ function displayDate(value?: string | null): string {
|
||||
</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>
|
||||
<strong>{{ activeReportLink?.label || reportPreview.relativePath || 'Report' }}</strong>
|
||||
<button class="btn btn-secondary" type="button" data-testid="mdtodo-report-fullscreen" @click="showReportFullscreen = true">全屏</button>
|
||||
</header>
|
||||
<div class="markdown-body" v-html="reportPreviewHtml" />
|
||||
</article>
|
||||
</aside>
|
||||
</section>
|
||||
</section>
|
||||
<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>
|
||||
@@ -893,8 +882,24 @@ function displayDate(value?: string | null): string {
|
||||
<p v-else>暂无关联 session。</p>
|
||||
</section>
|
||||
</template>
|
||||
</aside>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<template #right>
|
||||
<aside v-if="reportPreview" class="data-panel report-sidebar" data-testid="mdtodo-report-sidebar" :data-collapsed="reportPaneCollapsed">
|
||||
<header class="report-sidebar-header">
|
||||
<button class="icon-button" type="button" data-testid="mdtodo-report-sidebar-toggle" :aria-label="reportPaneCollapsed ? '展开报告侧栏' : '收起报告侧栏'" @click="reportPaneCollapsed = !reportPaneCollapsed">{{ reportPaneCollapsed ? '‹' : '›' }}</button>
|
||||
<strong v-if="!reportPaneCollapsed">{{ activeReportLink?.label || reportPreview.relativePath || 'Report' }}</strong>
|
||||
<div v-if="!reportPaneCollapsed" class="report-sidebar-actions">
|
||||
<button class="btn btn-secondary" type="button" data-testid="mdtodo-report-fullscreen" @click="showReportFullscreen = true">全屏</button>
|
||||
<button class="icon-button" type="button" aria-label="关闭报告预览" data-testid="mdtodo-report-close" @click="closeReportPreview()">×</button>
|
||||
</div>
|
||||
</header>
|
||||
<article v-if="!reportPaneCollapsed" class="report-preview" data-testid="mdtodo-report-preview">
|
||||
<div class="markdown-body" v-html="reportPreviewHtml" />
|
||||
</article>
|
||||
</aside>
|
||||
</template>
|
||||
</SplitWorkspaceLayout>
|
||||
</template>
|
||||
|
||||
<div v-if="showInfo" class="modal-backdrop" data-testid="mdtodo-info-popover" role="dialog" aria-modal="true" aria-label="MDTODO 摘要" @click.self="showInfo = false">
|
||||
@@ -962,7 +967,7 @@ function displayDate(value?: string | null): string {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mdtodo-page { display: grid; min-width: 0; gap: 10px; }
|
||||
.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: 44px; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #d8e2df; padding: 0 2px 8px; }
|
||||
.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: 20px; line-height: 1.1; }
|
||||
@@ -983,15 +988,12 @@ function displayDate(value?: string | null): string {
|
||||
.source-error { margin: 0; border-radius: 6px; padding: 8px 10px; font-size: 13px; }
|
||||
.source-message { border: 1px solid #bbf7d0; background: #f0fdf4; color: #166534; }
|
||||
.source-error { border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; }
|
||||
.mdtodo-workspace { display: grid; min-height: min(760px, calc(100dvh - 158px)); grid-template-columns: minmax(44px, var(--mdtodo-task-pane-width, 30%)) 6px minmax(0, 1fr); gap: 8px; }
|
||||
.task-pane-resizer { border-inline: 1px solid transparent; border-radius: 999px; background: linear-gradient(90deg, transparent 2px, #cbd8d2 2px, #cbd8d2 4px, transparent 4px); cursor: col-resize; }
|
||||
.mdtodo-workspace[data-tree-collapsed="true"] { grid-template-columns: 44px 0 minmax(0, 1fr); }
|
||||
.mdtodo-workspace[data-tree-collapsed="true"] .task-pane-resizer { pointer-events: none; opacity: 0; }
|
||||
.mdtodo-workspace { min-height: 0; height: 100%; }
|
||||
.mdtodo-task-panel,
|
||||
.mdtodo-detail-panel,
|
||||
.mdtodo-error { display: grid; min-width: 0; min-height: 0; align-content: start; gap: 12px; padding: 12px; overflow: auto; }
|
||||
.mdtodo-task-panel[data-collapsed="true"] { justify-items: center; padding: 8px 4px; overflow: hidden; }
|
||||
.mdtodo-detail-panel { align-content: stretch; grid-template-rows: auto minmax(0, 1fr) auto auto; overflow: hidden; }
|
||||
.mdtodo-detail-panel { height: 100%; align-content: stretch; grid-template-rows: auto minmax(0, 1fr) auto auto; overflow: hidden; }
|
||||
.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; }
|
||||
@@ -1016,7 +1018,7 @@ function displayDate(value?: string | null): string {
|
||||
.task-title-read:hover { background: #f4fbf8; }
|
||||
.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; grid-template-columns: minmax(0, 1fr) minmax(260px, 0.32fr); gap: 10px; }
|
||||
.task-detail-content { display: grid; min-width: 0; min-height: 0; grid-template-columns: minmax(0, 1fr); gap: 10px; overflow: hidden; }
|
||||
.task-detail-list,
|
||||
.summary-list { display: grid; gap: 8px; margin: 0; }
|
||||
.task-detail-list div,
|
||||
@@ -1031,7 +1033,7 @@ function displayDate(value?: string | null): string {
|
||||
.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; }
|
||||
.status-inline { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; }
|
||||
.task-editor { display: grid; min-width: 0; min-height: 0; grid-template-rows: auto minmax(0, 1fr) auto auto auto; gap: 10px; border: 1px solid #d8e2df; border-radius: 8px; background: #fbfefd; padding: 12px; }
|
||||
.task-editor { display: grid; min-width: 0; min-height: 0; grid-template-rows: auto minmax(0, 1fr) auto auto auto; gap: 10px; border: 1px solid #d8e2df; border-radius: 8px; background: #fbfefd; padding: 12px; overflow: hidden; }
|
||||
.editor-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(132px, 0.32fr); gap: 10px; }
|
||||
.task-editor label { display: grid; min-width: 0; gap: 5px; color: #52615c; font-size: 12px; font-weight: 800; }
|
||||
.task-editor input,
|
||||
@@ -1040,9 +1042,10 @@ function displayDate(value?: string | null): string {
|
||||
.task-editor textarea { min-height: 72px; resize: vertical; line-height: 1.4; }
|
||||
.editor-block { display: grid; gap: 5px; }
|
||||
.task-body-section,
|
||||
.report-section { display: grid; min-width: 0; min-height: 0; gap: 8px; }
|
||||
.report-link-section,
|
||||
.report-sidebar { display: grid; min-width: 0; min-height: 0; gap: 8px; }
|
||||
.task-body-section header,
|
||||
.report-section header,
|
||||
.report-link-section header,
|
||||
.report-preview 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; }
|
||||
.inline-body-editor { display: grid; gap: 8px; }
|
||||
@@ -1056,15 +1059,20 @@ function displayDate(value?: string | null): string {
|
||||
.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; }
|
||||
.task-body-rendered { min-height: min(520px, calc(100dvh - 330px)); overflow: auto; border: 1px solid #d8e2df; border-radius: 6px; background: #fff; padding: 12px; }
|
||||
.task-body-rendered { min-height: 0; overflow: auto; border: 1px solid #d8e2df; border-radius: 6px; background: #fff; padding: 12px; }
|
||||
.empty-inline { margin: 0; color: #64706b; }
|
||||
.report-section { grid-template-rows: auto auto auto minmax(0, 1fr); border: 1px solid #d8e2df; border-radius: 8px; background: #fff; padding: 10px; overflow: hidden; }
|
||||
.report-link-section { grid-template-rows: auto auto auto; border: 1px solid #d8e2df; border-radius: 8px; background: #fff; padding: 10px; overflow: auto; }
|
||||
.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; }
|
||||
.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; }
|
||||
.report-preview { display: grid; min-height: 0; gap: 8px; border-top: 1px solid #e2ece8; padding-top: 8px; }
|
||||
.report-sidebar { height: 100%; grid-template-rows: auto minmax(0, 1fr); border: 1px solid #d8e2df; border-radius: 8px; background: #fff; padding: 10px; overflow: hidden; }
|
||||
.report-sidebar[data-collapsed="true"] { justify-items: center; padding: 8px 4px; }
|
||||
.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 strong { 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 > .markdown-body { min-height: 0; overflow: auto; }
|
||||
.task-create-box { display: grid; gap: 8px; border-top: 1px solid #e2ece8; padding-top: 10px; }
|
||||
.editor-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
@@ -1082,8 +1090,8 @@ function displayDate(value?: string | null): string {
|
||||
.mdtodo-dialog h2 { margin: 0; font-size: 18px; }
|
||||
.task-create-dialog { width: min(620px, calc(100vw - 32px)); }
|
||||
.task-create-dialog 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; resize: vertical; }
|
||||
.report-fullscreen-dialog { width: min(1120px, calc(100vw - 32px)); min-height: min(780px, calc(100dvh - 80px)); align-content: start; }
|
||||
.report-fullscreen-body { max-height: calc(100dvh - 170px); overflow: auto; border: 1px solid #e2e8f0; border-radius: 6px; padding: 14px; }
|
||||
.report-fullscreen-dialog { 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; overflow: hidden; }
|
||||
.report-fullscreen-body { min-height: 0; height: 100%; overflow: auto; border: 1px solid #e2e8f0; border-radius: 6px; padding: 14px; }
|
||||
.source-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.source-form label { display: grid; min-width: 0; gap: 5px; color: #52615c; font-size: 12px; font-weight: 800; }
|
||||
.source-form footer { display: flex; grid-column: 1 / -1; flex-wrap: wrap; gap: 8px; }
|
||||
@@ -1092,13 +1100,13 @@ function displayDate(value?: string | null): string {
|
||||
@media (max-width: 980px) {
|
||||
.mdtodo-toolbar,
|
||||
.mdtodo-workspace { grid-template-columns: 1fr; }
|
||||
.task-pane-resizer { display: none; }
|
||||
.toolbar-actions { justify-content: flex-start; }
|
||||
.task-tools { width: 100%; grid-template-columns: 1fr; }
|
||||
.mdtodo-workspace { min-height: 0; }
|
||||
.mdtodo-page { height: auto; max-height: none; overflow: visible; }
|
||||
.mdtodo-workspace { min-height: 0; height: auto; }
|
||||
.mdtodo-task-panel[data-collapsed="true"] { justify-items: stretch; }
|
||||
.task-detail-content { grid-template-columns: 1fr; }
|
||||
.report-section { max-height: none; }
|
||||
.report-sidebar { min-height: 420px; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.source-form { grid-template-columns: 1fr; }
|
||||
|
||||
Reference in New Issue
Block a user