Domphy

sidebar06

A Sidebar block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call sidebar06() with no arguments for a working demo, or edit the code below live.

Implementation notes

Floating-dropdown nav built on @domphy/ui's popover()+menu(). Only one dropdown open at a time via cross-subscribed State<boolean> instances (opening one closes all siblings). Trigger row gets accent background/foreground via the '&[aria-expanded=true]' CSS selector. Opt-in card + footer included. Same off-canvas mobile shell as sidebar05. PARTIAL because responsive dropdown placement (below-trailing on mobile vs beside-leading on desktop) is approximated with a matchMedia listener flipping a shared placement State between 'bottom-end'/'right-start', combined with @domphy/floating's flip()/shift() middleware for on-screen collision handling — not a bespoke anchor-engine breakpoint rule. Doctor self-check: 0 issues.

Status: partial · Reference: shadcn/ui original

// shadcn/ui "sidebar-06" — clean-room reimplementation from the public behavior
// description only (no upstream source viewed). Nav rows never expand inline;
// clicking a top-level row opens a floating dropdown of its children instead,
// keeping the sidebar's vertical rhythm constant. Only one dropdown is open
// at a time, and its placement flips between "below" (mobile) and "beside"
// (desktop) so it never collides with the sidebar's own edge.

import type { DomphyElement, ElementNode, Listener } from "@domphy/core";
import { toState } from "@domphy/core";
import { button, menu, popover, small, strong } from "@domphy/ui";

/** The two placements this sidebar's dropdowns flip between (subset of
 * @domphy/floating's full `Placement` union — narrowed locally so this
 * package doesn't need a direct dependency on @domphy/floating). */
type DropdownPlacement = "right-start" | "bottom-end";
import { themeColor, themeDensity, themeSpacing } from "@domphy/theme";
import {
  ICON_BAR_CHART,
  ICON_FOLDER,
  ICON_GRID,
  ICON_INBOX,
  ICON_MARK,
  ICON_MORE,
  sidebarBackdrop,
  sidebarIcon,
  sidebarMainContent,
  sidebarStickyHeader,
  type SidebarBreadcrumbItem,
} from "./sidebar05-08-shared.js";

/** A plain-text child row inside a top-level item's floating dropdown. */
type Sidebar06ChildLink = { title: string; href?: string };

/** A top-level nav row whose children live in a floating dropdown, not inline. */
type Sidebar06NavItem = {
  title: string;
  icon?: string;
  items: Sidebar06ChildLink[];
};

/** Small bordered opt-in card shown near the bottom of the sidebar content. */
type Sidebar06OptInCard = {
  title: string;
  description: string;
  buttonLabel: string;
  onSubmit?: () => void;
};

type Sidebar06Props = {
  header?: { icon?: string; title?: string; subtitle?: string };
  navItems?: Sidebar06NavItem[];
  optInCard?: Sidebar06OptInCard | null;
  breadcrumbItems?: SidebarBreadcrumbItem[];
  children?: DomphyElement | DomphyElement[];
};

const DEFAULT_NAV_ITEMS: Sidebar06NavItem[] = [
  {
    title: "Playground",
    icon: ICON_GRID,
    items: [{ title: "History" }, { title: "Starred" }, { title: "Settings" }],
  },
  {
    title: "Models",
    icon: ICON_INBOX,
    items: [{ title: "Genesis" }, { title: "Explorer" }, { title: "Quantum" }],
  },
  {
    title: "Documentation",
    icon: ICON_BAR_CHART,
    items: [
      { title: "Introduction" },
      { title: "Get Started" },
      { title: "Tutorials" },
      { title: "Changelog" },
    ],
  },
  {
    title: "Settings",
    icon: ICON_FOLDER,
    items: [
      { title: "General" },
      { title: "Team" },
      { title: "Billing" },
      { title: "Limits" },
    ],
  },
];

const DEFAULT_OPT_IN_CARD: Sidebar06OptInCard = {
  title: "Subscribe",
  description: "Get the latest updates delivered to your inbox.",
  buttonLabel: "Subscribe",
};

/**
 * shadcn/ui "sidebar-06" — a nav where each top-level item opens a floating
 * dropdown of its children (instead of an inline accordion). Call with no
 * arguments for a fully working demo.
 */
function sidebar06(props: Sidebar06Props = {}): DomphyElement<"div"> {
  const {
    header = { icon: ICON_MARK, title: "Acme Inc", subtitle: "v1.0.0" },
    navItems = DEFAULT_NAV_ITEMS,
    optInCard = DEFAULT_OPT_IN_CARD,
    breadcrumbItems = [{ label: "Models" }, { label: "Genesis" }],
    children,
  } = props;

  const sidebarOpen = toState(true);

  // Responsive dropdown placement: below-and-trailing on mobile, beside-and-
  // leading on desktop, so the floating panel never collides with the
  // sidebar's own edge. Shared by every row's popover.
  const placement = toState<DropdownPlacement>("right-start");

  // Only one dropdown open at a time: opening any row's popover closes all
  // the others (each row owns its own `open` State, cross-wired below).
  const openStates = navItems.map(() => toState(false));
  openStates.forEach((state, index) => {
    state.addListener((value) => {
      if (!value) return;
      openStates.forEach((other, otherIndex) => {
        if (otherIndex !== index && other.get()) other.set(false);
      });
    });
  });

  const navRows: DomphyElement<"li">[] = navItems.map((item, index) => {
    const dropdownContent: DomphyElement<"div"> = {
      div: null,
      style: { minWidth: themeSpacing(40) },
      $: [
        menu({
          items: item.items.map((child, childIndex) => ({
            label: child.title,
            key: `${item.title}-${childIndex}`,
          })),
        }),
      ],
    } as unknown as DomphyElement<"div">;

    return {
      li: [
        {
          button: [
            ...(item.icon ? [sidebarIcon(item.icon)] : []),
            { span: item.title, style: { flex: "1", textAlign: "left" } } as unknown as DomphyElement,
            sidebarIcon(ICON_MORE),
          ],
          type: "button",
          ariaLabel: `${item.title} menu`,
          style: {
            display: "flex",
            alignItems: "center",
            width: "100%",
            gap: (l: Listener) => themeSpacing(themeDensity(l) * 2),
            paddingBlock: (l: Listener) => themeSpacing(themeDensity(l) * 2),
            paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
            borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 1),
            border: "none",
            cursor: "pointer",
            color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
            backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
            "&:hover": {
              backgroundColor: (l: Listener) => themeColor(l, "shift-2", "neutral"),
            },
            "&[aria-expanded=true]": {
              backgroundColor: (l: Listener) => themeColor(l, "shift-3", "primary"),
              color: (l: Listener) => themeColor(l, "shift-12", "primary"),
            },
          },
          $: [
            popover({
              open: openStates[index],
              placement,
              content: dropdownContent,
            }),
          ],
        } as unknown as DomphyElement,
      ],
      _key: item.title ?? index,
    } as DomphyElement<"li">;
  });

  const optInCardElement: DomphyElement<"div"> | null = optInCard
    ? ({
        div: [
          { strong: optInCard.title, $: [strong({ color: "neutral" })] } as unknown as DomphyElement,
          { small: optInCard.description, $: [small({ color: "neutral" })] } as unknown as DomphyElement,
          {
            button: optInCard.buttonLabel,
            type: "button",
            onClick: () => optInCard.onSubmit?.(),
            style: { width: "100%" },
            $: [button({ color: "primary" })],
          } as unknown as DomphyElement,
        ],
        dataTone: "shift-2",
        style: {
          display: "flex",
          flexDirection: "column",
          gap: themeSpacing(2),
          margin: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          padding: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 2),
          outline: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
          outlineOffset: "-1px",
          backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
        },
      } as unknown as DomphyElement<"div">)
    : null;

  const asideElement: DomphyElement<"aside"> = {
    aside: [
      {
        div: [
          {
            span: header.icon ?? ICON_MARK,
            dataTone: "shift-0",
            style: {
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              width: themeSpacing(8),
              height: themeSpacing(8),
              flexShrink: "0",
              borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 2),
              backgroundColor: (l: Listener) => themeColor(l, "inherit", "primary"),
              color: (l: Listener) => themeColor(l, "shift-10", "primary"),
            },
          } as unknown as DomphyElement,
          {
            div: [
              { strong: header.title ?? "Acme Inc", $: [strong({ color: "neutral" })] } as unknown as DomphyElement,
              { small: header.subtitle ?? "v1.0.0", $: [small({ color: "neutral" })] } as unknown as DomphyElement,
            ],
            style: { display: "flex", flexDirection: "column", gap: themeSpacing(0.5), minWidth: "0" },
          } as unknown as DomphyElement,
        ],
        style: {
          display: "flex",
          alignItems: "center",
          gap: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          flexShrink: "0",
          padding: (l: Listener) => themeSpacing(themeDensity(l) * 4),
        },
      } as unknown as DomphyElement,
      {
        nav: [{ ul: navRows, style: { listStyle: "none", margin: "0", padding: "0", display: "flex", flexDirection: "column", gap: themeSpacing(0.5) } } as unknown as DomphyElement],
        style: {
          flex: "1",
          minHeight: "0",
          overflowY: "auto",
          paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
        },
      } as unknown as DomphyElement,
      ...(optInCardElement ? [optInCardElement] : []),
      {
        footer: [
          { small: "© Acme Inc.", $: [small({ color: "neutral" })] } as unknown as DomphyElement,
        ],
        style: {
          flexShrink: "0",
          paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 4),
          paddingBlock: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          borderTop: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
          color: (l: Listener) => themeColor(l, "shift-7", "neutral"),
        },
      } as unknown as DomphyElement,
    ],
    dataTone: "shift-2",
    _onMount: (node: ElementNode) => {
      if (typeof window === "undefined" || !window.matchMedia) return;
      const media = window.matchMedia("(max-width: 768px)");
      const apply = () => placement.set(media.matches ? "bottom-end" : "right-start");
      apply();
      const listener = () => apply();
      media.addEventListener("change", listener);
      node.addHook("Remove", () => media.removeEventListener("change", listener));
    },
    style: {
      display: "flex",
      flexDirection: "column",
      flexShrink: "0",
      width: (l: Listener) => (sidebarOpen.get(l) ? themeSpacing(64) : "0px"),
      overflow: "hidden",
      transition: "width 0.2s linear",
      borderInlineEnd: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
      backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
      "@media (max-width: 768px)": {
        position: "fixed",
        insetBlock: "0",
        insetInlineStart: "0",
        zIndex: "15",
        width: themeSpacing(72),
        transform: (l: Listener) => (sidebarOpen.get(l) ? "translateX(0)" : "translateX(-100%)"),
        transition: "transform 0.2s ease",
        boxShadow: (l: Listener) => `0 0 ${themeSpacing(6)} ${themeColor(l, "shift-3", "neutral")}`,
      },
    },
  } as unknown as DomphyElement<"aside">;

  const mainElement: DomphyElement<"main"> = {
    main: [
      sidebarStickyHeader({
        onToggle: () => sidebarOpen.set(!sidebarOpen.get()),
        breadcrumbItems,
      }),
      sidebarMainContent(children),
    ],
    style: {
      display: "flex",
      flexDirection: "column",
      flex: "1",
      minWidth: "0",
      minHeight: "0",
      overflow: "auto",
    },
  } as unknown as DomphyElement<"main">;

  return {
    div: [asideElement, mainElement, sidebarBackdrop(sidebarOpen, () => sidebarOpen.set(false))],
    style: {
      display: "flex",
      height: "100dvh",
      overflow: "hidden",
      position: "relative",
    },
  } as unknown as DomphyElement<"div">;
}

export { sidebar06 };
export type { Sidebar06ChildLink, Sidebar06NavItem, Sidebar06OptInCard, Sidebar06Props };

← Back to shadcn/ui catalog