Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { css, cva } from "@hashintel/ds-helpers/css";

import { InfoIconTooltip } from "../tooltip";
import type { SubView } from "./types";

const tabsContainerStyle = css({
display: "flex",
alignItems: "center",
gap: "[4px]",
});

const tabButtonStyle = cva({
base: {
fontSize: "[11px]",
fontWeight: "[500]",
padding: "[4px 10px]",
textTransform: "uppercase",
borderRadius: "[3px]",
border: "none",
cursor: "pointer",
transition: "[all 0.3s ease]",
background: "[transparent]",
},
variants: {
active: {
true: {
opacity: "[1]",
backgroundColor: "[rgba(0, 0, 0, 0.08)]",
color: "core.gray.90",
},
false: {
opacity: "[0.6]",
color: "core.gray.60",
_hover: {
opacity: "[1]",
backgroundColor: "[rgba(0, 0, 0, 0.04)]",
color: "core.gray.80",
},
},
},
},
});

const contentStyle = css({
fontSize: "[12px]",
padding: "[12px 12px]",
flex: "[1]",
overflowY: "auto",
});

interface TabButtonProps {
subView: SubView;
isActive: boolean;
onClick: () => void;
}

const TabButton: React.FC<TabButtonProps> = ({ subView, isActive, onClick }) => {
return (
<button
type="button"
onClick={onClick}
className={tabButtonStyle({ active: isActive })}
aria-selected={isActive}
role="tab"
>
{subView.title}
{subView.tooltip && <InfoIconTooltip tooltip={subView.tooltip} />}
</button>
);
};

interface HorizontalTabsContainerProps {
/** Array of subviews to display as tabs */
subViews: SubView[];
/** ID of the currently active tab */
activeTabId: string;
/** Callback when a tab is selected */
onTabChange: (tabId: string) => void;
}

/**
* Container that displays subviews as horizontal tabs.
* Used in the BottomPanel for Diagnostics, Parameters, and Simulation Settings.
*
* This component returns both the tabs header and the content area as separate
* parts that can be composed into the parent layout.
*/
export const HorizontalTabsContainer: React.FC<HorizontalTabsContainerProps> = ({
subViews,
activeTabId,
onTabChange,
}) => {
const activeSubView = subViews.find((sv) => sv.id === activeTabId) ?? subViews[0];

if (!activeSubView) {
return null;
}

const Component = activeSubView.component;

return (
<>
{/* Tab Header */}
<div className={tabsContainerStyle} role="tablist">
{subViews.map((subView) => (
<TabButton
key={subView.id}
subView={subView}
isActive={activeTabId === subView.id}
onClick={() => onTabChange(subView.id)}
/>
))}
</div>

{/* Content */}
<div className={contentStyle} role="tabpanel" aria-labelledby={activeTabId}>
<Component />
</div>
</>
);
};

/**
* Renders just the tab bar portion of the horizontal tabs.
* Useful when you need to compose the tabs header separately from the content.
*/
export const HorizontalTabsHeader: React.FC<{
subViews: SubView[];
activeTabId: string;
onTabChange: (tabId: string) => void;
}> = ({ subViews, activeTabId, onTabChange }) => {
return (
<div className={tabsContainerStyle} role="tablist">
{subViews.map((subView) => (
<TabButton
key={subView.id}
subView={subView}
isActive={activeTabId === subView.id}
onClick={() => onTabChange(subView.id)}
/>
))}
</div>
);
};

/**
* Returns the header action for the currently active tab.
* Used to render custom actions (like add buttons) in the panel header.
*/
export const HorizontalTabsHeaderAction: React.FC<{
subViews: SubView[];
activeTabId: string;
}> = ({ subViews, activeTabId }) => {
const activeSubView =
subViews.find((sv) => sv.id === activeTabId) ?? subViews[0];

if (!activeSubView?.renderHeaderAction) {
return null;
}

return <>{activeSubView.renderHeaderAction()}</>;
};

/**
* Renders just the content portion of the horizontal tabs.
* Useful when you need to compose the content separately from the tabs header.
*/
export const HorizontalTabsContent: React.FC<{
subViews: SubView[];
activeTabId: string;
}> = ({ subViews, activeTabId }) => {
const activeSubView = subViews.find((sv) => sv.id === activeTabId) ?? subViews[0];

if (!activeSubView) {
return null;
}

const Component = activeSubView.component;

return (
<div className={contentStyle} role="tabpanel" aria-labelledby={activeTabId}>
<Component />
</div>
);
};

9 changes: 9 additions & 0 deletions libs/@hashintel/petrinaut/src/components/sub-view/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type { SubView, SubViewResizeConfig } from "./types";
export { VerticalSubViewsContainer } from "./vertical-sub-views-container";
export {
HorizontalTabsContainer,
HorizontalTabsHeader,
HorizontalTabsHeaderAction,
HorizontalTabsContent,
} from "./horizontal-tabs-container";

46 changes: 46 additions & 0 deletions libs/@hashintel/petrinaut/src/components/sub-view/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ComponentType, ReactNode } from "react";

/**
* Configuration for resizable subviews.
*/
export interface SubViewResizeConfig {
/** Default height when expanded (in pixels) */
defaultHeight: number;
/** Minimum height constraint (in pixels) */
minHeight?: number;
/** Maximum height constraint (in pixels) */
maxHeight?: number;
}

/**
* SubView represents a single view that can be displayed in either:
* - A vertical collapsible section (LeftSideBar)
* - A horizontal tab (BottomPanel)
*
* This abstraction allows views to be easily moved between panels.
*/
export interface SubView {
/** Unique identifier for the subview */
id: string;
/** Title displayed in the section header or tab */
title: string;
/** Optional tooltip shown when hovering over the title/tab */
tooltip?: string;
/** The component to render for this subview */
component: ComponentType;
/**
* Optional render function for the header right side (e.g., add button).
* Only used in vertical (collapsible) layout.
*/
renderHeaderAction?: () => ReactNode;
/**
* Whether this subview should grow to fill available space.
* Only affects vertical layout. Defaults to false.
*/
flexGrow?: boolean;
/**
* Configuration for making the subview resizable when expanded.
* Only affects vertical layout. When set, the section can be resized by dragging its bottom edge.
*/
resizable?: SubViewResizeConfig;
}
Loading
Loading