diff --git a/src/components/composed/DataAppShell/DataAppShell.stories.tsx b/src/components/composed/DataAppShell/DataAppShell.stories.tsx index f0deb744..7ab95557 100644 --- a/src/components/composed/DataAppShell/DataAppShell.stories.tsx +++ b/src/components/composed/DataAppShell/DataAppShell.stories.tsx @@ -85,7 +85,7 @@ function UserMenuButton({ name, userRole, expanded = false }: UserMenuButtonProp ) : ( ); @@ -282,62 +266,6 @@ function WorkflowPanel({ ); } -// ── Data count pills ───────────────────────────────────────────────────────── - -interface DataCount { - label: string; - count: number; - variant?: "default" | "outline" | "primary"; - onClick?: () => void; -} - -const countPillVariants = cva( - "inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium tabular-nums transition-colors", - { - variants: { - variant: { - default: "bg-muted text-muted-foreground", - outline: "bg-transparent border border-border text-foreground", - primary: "bg-primary/10 border border-primary/30 text-primary", - }, - clickable: { - true: "cursor-pointer hover:bg-primary/15", - false: "cursor-default", - }, - }, - defaultVariants: { variant: "default", clickable: false }, - }, -); - -function DataCountPills({ dataCounts }: { dataCounts: DataCount[] }) { - if (dataCounts.length === 0) return null; - return ( -
- {dataCounts.map((dc, i) => { - const pillClass = cn(countPillVariants({ variant: dc.variant ?? "outline", clickable: !!dc.onClick })); - const pillContent = ( - <> - {dc.label} - {dc.count.toLocaleString()} - - ); - return ( - - {i > 0 && {"\u2192"}} - {dc.onClick ? ( - - ) : ( -
{pillContent}
- )} -
- ); - })} -
- ); -} - // ============================================================================= // Meta // ============================================================================= @@ -368,16 +296,14 @@ const htsNavGroups: NavGroup[] = [ const htsWorkflowSteps: WorkflowStep[] = [ { id: "data-overview", - label: "Data Overview", + label: "Step 1 Name", icon: LayoutGrid, isActive: true, - inputCount: 649568, - outputCount: 645396, }, - { id: "global-filtering", label: "Global Filtering", icon: Filter, inputCount: 645396, outputCount: 4803 }, - { id: "explore-clusters", label: "Explore Clusters", icon: Library, inputCount: 3917, outputCount: 20 }, - { id: "review-compound", label: "Review Selection", icon: Search, inputCount: 20, outputCount: 15 }, - { id: "export-list", label: "Export Primary List", icon: Download, inputCount: 15 }, + { id: "global-filtering", label: "Step 2 Name", icon: Filter }, + { id: "explore-clusters", label: "Step 3 Name", icon: Library }, + { id: "review-compound", label: "Step 4 Name", icon: Search }, + { id: "export-list", label: "Step 5 Name", icon: Download }, ]; const htsBreadcrumbs = [ @@ -400,17 +326,9 @@ const DefaultShell = ({ initialCollapsed = false }: { initialCollapsed?: boolean onClick: () => setActiveStepId(s.id), })); - const activeStep = steps.find((s) => s.isActive); const activeStepIndex = steps.findIndex((s) => s.isActive); const isLastStep = activeStepIndex === steps.length - 1; - - const dataCounts: DataCount[] = - activeStep?.inputCount == null || activeStep?.outputCount == null - ? [] - : [ - { label: "INPUT", count: activeStep.inputCount, variant: "outline" }, - { label: "Output", count: activeStep.outputCount, variant: "primary" }, - ]; + const isFirstStep = activeStepIndex <= 0; return ( - + {!isFirstStep && ( + + )} ) @@ -620,7 +542,7 @@ export const Interactive: Story = { await step("Interactive shell renders", async () => { expect(canvas.getByText("HTS")).toBeInTheDocument(); - expect(canvas.getAllByText("Data Overview").length).toBeGreaterThan(0); + expect(canvas.getAllByText("Step 1 Name").length).toBeGreaterThan(0); }); }, parameters: { @@ -829,11 +751,10 @@ export const WorkflowPanelInteractions: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); - await step("Expanded panel shows step labels and counts", async () => { + await step("Expanded panel shows step labels", async () => { expect(canvas.getByText("Step Alpha")).toBeInTheDocument(); expect(canvas.getByText("Step Beta")).toBeInTheDocument(); expect(canvas.getByText("Disabled")).toBeInTheDocument(); - expect(canvas.getByText("1K")).toBeInTheDocument(); }); await step("Clicking Step Beta makes it the active step", async () => { @@ -909,9 +830,9 @@ export const MultipleNavGroups: Story = { const canvas = within(canvasElement); await step("All pages from both groups are visible", async () => { - expect(canvas.getByText("Project")).toBeInTheDocument(); - expect(canvas.getByText("Explorer")).toBeInTheDocument(); - expect(canvas.getAllByText("Filters").length).toBeGreaterThan(0); + expect(canvas.getByRole("button", { name: "Project" })).toBeInTheDocument(); + expect(canvas.getByRole("button", { name: "Explorer" })).toBeInTheDocument(); + expect(canvas.getByRole("button", { name: "Filters" })).toBeInTheDocument(); }); await step("A separator divides the two groups in the icon rail", async () => { @@ -924,7 +845,7 @@ export const MultipleNavGroups: Story = { await step("Active page icon has primary highlight", async () => { // Filters page is isActive — its icon container has bg-primary/10 const rail = canvasElement.querySelector("[data-slot='data-app-sidebar-rail']"); - const activePage = within(rail!).getByText("Filters").closest("button"); + const activePage = within(rail!).getByRole("button", { name: "Filters" }); const iconContainer = activePage?.querySelector(".bg-primary\\/10"); expect(iconContainer).toBeInTheDocument(); }); @@ -1221,16 +1142,16 @@ export const CompactProperty: Story = { await step("Compact icon rail renders on desktop (hidden on mobile)", async () => { const rail = canvasElement.querySelector("[data-slot='data-app-sidebar-rail']"); expect(rail).toBeInTheDocument(); - // Icon rail should have width of 60px - expect(rail).toHaveClass("w-[60px]"); + // Icon rail should have width of 48px + expect(rail).toHaveClass("w-12"); }); - await step("Icon rail displays icons and labels stacked vertically", async () => { + await step("Icon rail displays icon-only nav buttons labelled via aria-label", async () => { const rail = canvasElement.querySelector("[data-slot='data-app-sidebar-rail']"); - // Labels should be present - expect(within(rail!).getByText("Project")).toBeInTheDocument(); - expect(within(rail!).getByText("Explorer")).toBeInTheDocument(); - expect(within(rail!).getByText("Filters")).toBeInTheDocument(); + // Labels are exposed as accessible names (aria-label), not visible text + expect(within(rail!).getByRole("button", { name: "Project" })).toBeInTheDocument(); + expect(within(rail!).getByRole("button", { name: "Explorer" })).toBeInTheDocument(); + expect(within(rail!).getByRole("button", { name: "Filters" })).toBeInTheDocument(); }); await step("Group labels are hidden in compact icon rail", async () => { @@ -1242,7 +1163,7 @@ export const CompactProperty: Story = { await step("Active page has primary background highlight in compact mode", async () => { const rail = canvasElement.querySelector("[data-slot='data-app-sidebar-rail']"); - const projectBtn = within(rail!).getByText("Project").closest("button"); + const projectBtn = within(rail!).getByRole("button", { name: "Project" }); const iconDiv = projectBtn?.querySelector(".bg-primary\\/10"); expect(iconDiv).toBeInTheDocument(); }); @@ -1261,7 +1182,7 @@ export const CompactProperty: Story = { // Icon rail has md:flex which means it's hidden on mobile expect(rail).toHaveClass("hidden", "md:flex"); const railStyles = window.getComputedStyle(rail!); - expect(railStyles.width).toBe("60px"); + expect(railStyles.width).toBe("48px"); }); await step("User menu is visible at bottom of icon rail", async () => { diff --git a/src/components/composed/DataAppShell/DataAppShell.tsx b/src/components/composed/DataAppShell/DataAppShell.tsx index a9714f64..748c6dd5 100644 --- a/src/components/composed/DataAppShell/DataAppShell.tsx +++ b/src/components/composed/DataAppShell/DataAppShell.tsx @@ -104,6 +104,8 @@ export interface DataAppShellProps { // -- Shell -- /** Slot rendered between the icon rail and the content (e.g. WorkflowPanel) */ sidebarPanel?: React.ReactNode; + /** Hide the desktop icon nav rail (e.g. when the sidebar panel is collapsed) */ + navRailHidden?: boolean; /** Main content area */ children: React.ReactNode; /** Additional className for the root container */ @@ -128,7 +130,7 @@ interface SidebarBodyProps | "userMenu" > { /** - * compact=true → narrow icon rail (60px): icons + tiny labels stacked, tooltips on hover + * compact=true → narrow icon rail (48px): icon-only nav buttons, tooltips on hover * compact=false → expanded mobile sheet (220px): icon + label side-by-side rows */ compact: boolean; @@ -145,7 +147,7 @@ const pageIconVariants = cva( false: "bg-transparent", }, compact: { - true: "w-9 h-9 hover:bg-muted", + true: "w-[30px] h-[30px] hover:bg-muted", false: "w-8 h-8", }, }, @@ -171,8 +173,8 @@ function SidebarBody({ {/* ── Header: app icon / name + version ──────────────────────────────── */}
@@ -187,8 +189,7 @@ function SidebarBody({ {/* Icon */} {appIcon ?? appName} @@ -289,7 +290,7 @@ function SidebarBody({ const iconEl = Icon ? ( @@ -313,22 +314,13 @@ function SidebarBody({ {page.label} @@ -385,7 +377,7 @@ function IconRailSidebar(props: Omit @@ -432,13 +424,13 @@ function TopNav({ ) : isClickable && item.onClick ? ( ) : ( - {item.label} + {item.label} )} @@ -492,6 +484,7 @@ function DataAppShell({ onHelpClick, headerActions, sidebarPanel, + navRailHidden = false, children, className, }: DataAppShellProps) { @@ -515,8 +508,8 @@ function DataAppShell({ data-slot="data-app-shell" className={cn("flex flex-row w-full h-screen overflow-hidden", className)} > - {/* Desktop icon rail (hidden on mobile) */} - + {/* Desktop icon rail (hidden on mobile, or when navRailHidden) */} + {!navRailHidden && } {/* Mobile sidebar Sheet */}