{row.map((component, index) => {
if (!component) return null;
-
const componentKey = component.id || `component-${index}`;
const isSeparator = component.type === 'separator';
-
return (
{
);
};
-
return (
{components.rows.map((row, index) => renderRow(row, index))}
);
};
-
export default memo(DynamicComponents);
diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx
index c2ba4ca67..cf9fbc424 100644
--- a/frontend/src/components/ui/sidebar.tsx
+++ b/frontend/src/components/ui/sidebar.tsx
@@ -1,9 +1,10 @@
'use client';
import { PanelLeft, PanelLeftClose } from 'lucide-react';
-import { Link, useLocation } from 'react-router-dom';
+
import * as React from 'react';
import { useEffect } from 'react';
+import { Link, useLocation } from 'react-router-dom';
import TableOfContents from '@/components/TableOfContents';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx
new file mode 100644
index 000000000..85d83beab
--- /dev/null
+++ b/frontend/src/components/ui/tabs.tsx
@@ -0,0 +1,53 @@
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/frontend/src/components/widgets/TabWidget.jsx b/frontend/src/components/widgets/TabWidget.jsx
new file mode 100644
index 000000000..d066c64d0
--- /dev/null
+++ b/frontend/src/components/widgets/TabWidget.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+
+import { Card } from '@/components/ui/card';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+
+import DynamicComponents from '../DynamicComponents';
+
+const TabWidget = ({ label, tabs = [], onComponentUpdate }) => {
+ const [activeTab, setActiveTab] = React.useState(tabs?.[0]?.title || '');
+
+ const normalizeComponents = (components, tabTitle) => {
+ if (!components) return [];
+
+ return components.map((component, index) => {
+ if (typeof component === 'string') {
+ return {
+ id: `tab-${tabTitle}-text-${hashString(component)}-${index}`,
+ type: 'text',
+ content: component,
+ markdown: component,
+ value: component,
+ };
+ }
+
+ const baseId = component.id || `comp-${index}`;
+ return {
+ ...component,
+ id: `tab-${tabTitle}-${baseId}`,
+ type: component.type || 'text',
+ };
+ });
+ };
+
+ const hashString = (str) => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = (hash << 5) - hash + str.charCodeAt(i);
+ hash |= 0;
+ }
+ return Math.abs(hash).toString(36);
+ };
+
+ return (
+
+ {label}
+
+
+ {tabs.map((tab) => (
+
+ {tab.title}
+
+ ))}
+
+
+ {tabs.map((tab) => {
+ const normalizedComponents = normalizeComponents(tab.components || [], tab.title);
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default TabWidget;
diff --git a/preswald/interfaces/__init__.py b/preswald/interfaces/__init__.py
index 5d4cdb5c0..fc6ca8639 100644
--- a/preswald/interfaces/__init__.py
+++ b/preswald/interfaces/__init__.py
@@ -20,6 +20,7 @@
sidebar,
slider,
spinner,
+ tab,
table,
text,
text_input,
diff --git a/preswald/interfaces/components.py b/preswald/interfaces/components.py
index 34673f0aa..71a4b0958 100644
--- a/preswald/interfaces/components.py
+++ b/preswald/interfaces/components.py
@@ -859,6 +859,38 @@ def sidebar(defaultopen: bool = False):
return component
+def tab(
+ label: str,
+ tabs: list[dict],
+ size: float = 1.0,
+) -> None:
+ """
+ tab() component that enables developers to
+ organize UI content into labeled tabs within their
+ Preswald apps—ideal for sectioning long dashboards,
+ multiple views, or split data insights.
+
+ Args:
+ label: Text to show on the tab-bar
+ tabs: Sections to be placed under the tab (including their title and component)
+ size: Customizable, full-width by default (=1.0)
+ """
+
+ service = PreswaldService.get_instance()
+ component_id = generate_stable_id("tab", label)
+
+ component = {
+ "type": "tab",
+ "id": component_id,
+ "label": label,
+ "size": size,
+ "tabs": tabs,
+ }
+
+ service.append_component(component)
+ return component
+
+
def table(
data: pd.DataFrame, title: str | None = None, limit: int | None = None
) -> dict: