Estado de fase (MASTER-00): MVP — alcance del MVP Shipaton sujeto a resultados de Gate 0 y PoC. Gobernanza: MASTER-00.
Frontend Spec: Animated Node Detail Inspector Drawer Composable (NodeInspector)
1. Overview & Purpose
This specification details the side-drawer component (NodeInspector) in :docugraph-app. Activated when a user taps a graph node on FE-03 GraphCanvas, it slides in smoothly from the screen margin (250ms cubic-bezier transition), displaying YAML frontmatter metadata, rendered Markdown preview, dependency link navigation chips, and a "Copy MCP Context Bundle" button that invokes BE-04 getTaskContext.
2. Component Signature & Props
package io.docugraph.app.ui.inspector
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun NodeInspector(
nodeId: String?,
isOpen: Boolean,
onClose: () -> Unit,
onNavigateToNode: (nodeId: String) -> Unit,
viewModel: NodeInspectorViewModel,
modifier: Modifier = Modifier
)
3. UI State Model & Data Classes
package io.docugraph.app.ui.inspector
import io.docugraph.backend.models.graph.GraphNodeDto
enum class InspectorTab {
FRONTMATTER,
MARKDOWN_PREVIEW,
TRACEABILITY_LINKS
}
data class NodeInspectorUiState(
val nodeDetail: GraphNodeDto? = null,
val activeTab: InspectorTab = InspectorTab.MARKDOWN_PREVIEW,
val upstreamLinks: List<String> = emptyList(),
val downstreamLinks: List<String> = emptyList(),
val isExtractingContext: Boolean = false,
val contextBundleMarkdown: String? = null,
val copySuccessBanner: Boolean = false
)
4. UI Layout Hierarchy & Composables
AnimatedVisibility (
visible = isOpen && nodeId != null,
enter = slideInHorizontally(initialOffsetX = { it }),
exit = slideOutHorizontally(targetOffsetX = { it })
)
└── Surface (Modifier.fillMaxHeight().width(380.dp), elevation = 8.dp)
└── Column (Padding 16.dp)
├── HeaderRow
│ ├── AtomicId Badge (e.g. "US-01") + Status Tag ("APPROVED")
│ └── IconButton (Close -> onClose())
├── Text (Title, style = TitleLarge)
├── Text (FilePath, style = BodySmall, color = Gray)
├── Spacer (Height 12.dp)
├── TabRow (selectedTabIndex = activeTab.ordinal)
│ ├── Tab ("Preview")
│ ├── Tab ("Frontmatter")
│ └── Tab ("Traceability")
├── HorizontalDivider()
├── Spacer (Height 12.dp)
├── Content (Weight 1.0f):
│ ├── if (Preview) MarkdownRenderer(content)
│ ├── if (Frontmatter) FrontmatterJsonView(frontmatterJson)
│ └── if (Traceability) TraceabilityLinksColumn
│ ├── Upstream Chips (OnClick = onNavigateToNode(id))
│ └── Downstream Chips (OnClick = onNavigateToNode(id))
├── Spacer (Height 16.dp)
└── Button ("Copy MCP Context Bundle", onClick = extractMcpContext)
└── Content: if (isExtractingContext) CircularProgressIndicator else Icon(Copy) + Text("Copy Context")
5. MCP Context Bundle Copy Interaction
fun extractMcpContext(taskId: String) {
viewModelScope.launch {
_uiState.update { it.copy(isExtractingContext = true) }
try {
val response = mcpService.getTaskContext(taskId = taskId, maxDepth = 5)
clipboardManager.setText(AnnotatedString(response.markdownBundle))
_uiState.update { it.copy(isExtractingContext = false, copySuccessBanner = true) }
} catch (e: Exception) {
_uiState.update { it.copy(isExtractingContext = false) }
}
}
}
6. Verification & Test Plan
- Animation Test: Verify 250ms slide-in/out transition smooth execution.
- UiTest: Tap upstream dependency chip, verify
onNavigateToNodeis called. Tap "Copy MCP Context Bundle", verify clipboard receives formatted markdown bundle.