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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions understand-anything-plugin/packages/core/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,17 @@ export const LayerSchema = z.object({
name: z.string(),
description: z.string(),
nodeIds: z.array(z.string()),
containers: z
.array(
z.object({
id: z.string().optional(),
name: z.string(),
nodeIds: z.array(z.string()),
group: z.string().optional(),
}),
)
.optional(),
tier: z.number().optional(),
});

export const TourStepSchema = z.object({
Expand Down
4 changes: 4 additions & 0 deletions understand-anything-plugin/packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export interface Layer {
name: string;
description: string;
nodeIds: string[];
/** Optional manual grouping of this layer's nodes into named feature containers. */
containers?: { id?: string; name: string; nodeIds: string[]; group?: string }[];
/** Optional vertical tier for the project overview (0 = top). */
tier?: number;
}

// TourStep (for learn mode)
Expand Down
2 changes: 2 additions & 0 deletions understand-anything-plugin/packages/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ function DashboardContent({
state.closeCodeViewer();
} else if (state.selectedNodeId) {
state.selectNode(null);
} else if (state.activeContainerId) {
state.exitContainer();
} else if (state.navigationLevel === "layer-detail") {
state.navigateToOverview();
} else if (state.tourActive) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { useI18n } from "../contexts/I18nContext";
export default function Breadcrumb() {
const navigationLevel = useDashboardStore((s) => s.navigationLevel);
const activeLayerId = useDashboardStore((s) => s.activeLayerId);
const activeContainerId = useDashboardStore((s) => s.activeContainerId);
const activeContainerName = useDashboardStore((s) => s.activeContainerName);
const exitContainer = useDashboardStore((s) => s.exitContainer);
const graph = useDashboardStore((s) => s.graph);
const navigateToOverview = useDashboardStore((s) => s.navigateToOverview);
const { t } = useI18n();
Expand All @@ -27,9 +30,22 @@ export default function Breadcrumb() {
{t.breadcrumb.project}
</button>
<span className="text-text-muted">›</span>
<span className="text-text-primary">
{activeLayer?.name ?? t.layer.defaultName}
</span>
{activeContainerId ? (
<>
<button
onClick={exitContainer}
className="text-gold hover:text-gold-bright transition-colors"
>
{activeLayer?.name ?? t.layer.defaultName}
</button>
<span className="text-text-muted">›</span>
<span className="text-text-primary">{activeContainerName}</span>
</>
) : (
<span className="text-text-primary">
{activeLayer?.name ?? t.layer.defaultName}
</span>
)}
<span className="text-text-muted ml-1 text-[10px] normal-case tracking-normal">
({t.breadcrumb.escBack})
</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { memo } from "react";
import { memo, useEffect } from "react";
import { Handle, Position, useUpdateNodeInternals } from "@xyflow/react";
import type { NodeProps, Node } from "@xyflow/react";
import { getLayerColor } from "./LayerLegend";

Expand All @@ -13,13 +14,19 @@ export interface ContainerNodeData extends Record<string, unknown> {
searchHitCount?: number;
isDiffAffected: boolean;
isFocusedViaChild: boolean;
onToggle: (containerId: string) => void;
onToggle: (containerId: string, name?: string) => void;
}

export type ContainerFlowNode = Node<ContainerNodeData, "container">;

function ContainerNodeComponent({ data, width, height }: NodeProps<ContainerFlowNode>) {
function ContainerNodeComponent({ id, data, width, height }: NodeProps<ContainerFlowNode>) {
const color = getLayerColor(data.colorIndex);
// Re-measure handles when the container resizes on expand/collapse —
// otherwise edges keep pointing at the stale (pre-expansion) bounds.
const updateNodeInternals = useUpdateNodeInternals();
useEffect(() => {
updateNodeInternals(id);
}, [id, width, height, data.isExpanded, updateNodeInternals]);

const borderColor = data.isDiffAffected
? "var(--color-diff-changed)"
Expand All @@ -33,7 +40,7 @@ function ContainerNodeComponent({ data, width, height }: NodeProps<ContainerFlow

const handleToggle = (e: React.SyntheticEvent) => {
e.stopPropagation();
data.onToggle(data.containerId);
data.onToggle(data.containerId, data.name);
};

return (
Expand All @@ -58,6 +65,11 @@ function ContainerNodeComponent({ data, width, height }: NodeProps<ContainerFlow
}
}}
>
{/* Invisible handles — without them React Flow cannot anchor
container→portal / container→container aggregated edges and
silently skips rendering them. */}
<Handle type="target" position={Position.Top} className="!opacity-0 !pointer-events-none !w-1 !h-1" />
<Handle type="source" position={Position.Bottom} className="!opacity-0 !pointer-events-none !w-1 !h-1" />
<div
className="flex items-center justify-between font-heading"
style={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface CustomNodeData extends Record<string, unknown> {
incomingCount?: number;
outgoingCount?: number;
tags?: string[];
dense?: boolean;
}

export type CustomFlowNode = Node<CustomNodeData, "custom">;
Expand Down Expand Up @@ -129,11 +130,38 @@ function CustomNodeComponent({

const name = data.label ?? "unnamed";
const truncatedName =
name.length > 24 ? name.slice(0, 22) + "..." : name;
name.length > 60 ? name.slice(0, 58) + "..." : name;

// Dense row variant for children of large expanded containers.
if (data.dense) {
return (
<div
className={`relative rounded-md bg-elevated border border-border-subtle ${extraClass} overflow-hidden cursor-pointer`}
style={{ width: 250, height: 44 }}
onClick={() => data.onNodeClick?.(id)}
title={data.summary}
>
<div
className="absolute left-0 top-0 bottom-0 w-1 rounded-l-md"
style={{ backgroundColor: barColor }}
/>
<Handle type="target" position={Position.Top} className="!opacity-0 !w-1 !h-1" />
<div className="pl-3 pr-2 h-full flex flex-col justify-center">
<div className="text-[12px] font-heading text-text-primary truncate" title={data.label}>
{name}
</div>
<div className="text-[9px] uppercase tracking-wider text-text-muted">
{data.nodeType}
</div>
</div>
<Handle type="source" position={Position.Bottom} className="!opacity-0 !w-1 !h-1" />
</div>
);
}

return (
<div
className={`relative rounded-lg bg-elevated border border-border-subtle ${extraClass} min-w-[180px] max-w-[220px] overflow-hidden transition-[box-shadow,outline,opacity,filter] duration-200 cursor-pointer shadow-[0_2px_8px_rgba(0,0,0,0.3)]`}
className={`relative rounded-lg bg-elevated border border-border-subtle ${extraClass} min-w-[240px] max-w-[310px] overflow-hidden transition-[box-shadow,outline,opacity,filter] duration-200 cursor-pointer shadow-[0_2px_8px_rgba(0,0,0,0.3)]`}
onClick={() => data.onNodeClick?.(id)}
>
{/* Left color bar */}
Expand Down Expand Up @@ -168,11 +196,11 @@ function CustomNodeComponent({
</div>
</div>

<div className="text-sm font-heading text-text-primary truncate" title={data.label}>
<div className="text-sm font-heading text-text-primary line-clamp-2 break-words" title={data.label}>
{truncatedName}
</div>

<div className="text-[11px] text-text-secondary mt-1 line-clamp-2 leading-tight">
<div className="text-[11px] text-text-secondary mt-1 line-clamp-3 leading-tight">
{data.summary}
</div>
</div>
Expand Down
Loading