Skip to content
Domphy

sidebar10

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

Props

PropTypeDescription
teamsSidebarTeam[]
favoritesSidebar10FavoriteItem[]
favoritesVisibleCountnumber
workspacesSidebar10Workspace[]
workspacesVisibleCountnumber
secondaryLinksSidebar10SecondaryLink[]
breadcrumbItemsSidebarBreadcrumbItem[]
childrenDomphyElement | DomphyElement[]
Implementation notes

Persistent workspace sidebar reusing the sidebar05-08 team-switcher/user-footer helpers, plus new Favorites (emoji rows with hover-reveal 'more' popover, real 'show more' reveal instead of a fixed count) and Workspaces (details-accordion with nested pages, hover-reveal 'add page' button, real 'show more' reveal) groups, and a 'sidebar in a popover' quick-actions menu (bordered, grouped icon+label rows) wired to both per-row more-buttons and the header's three-dot button. Gap: 'Ask AI' renders as an inert nav-style row rather than a live assistant (faking an LLM response would misrepresent capability); the hover-revealed 'add page' button stops the details toggle but doesn't create a real page (no page-creation flow was specified); the header's avatar-stack is decorative, not backed by live presence data. Direct-source-diff fix (2026-07-05): Header menu was an invented generic '…' menu — replaced with upstream's exact 4-group Notion page-actions menu, plus per-page emoji on workspace sub-pages.

Status: ported · Reference: shadcn/ui original

// shadcn/ui "sidebar-10" — clean-room reimplementation from the public
// behavior description only (no upstream source viewed). A persistent
// workspace sidebar (team switcher, favorites, nested workspace tree,
// secondary links) whose per-item overflow menu and the header's quick-action
// menu open as a floating popover styled like a miniature sidebar (multiple
// bordered sections, icon+label rows) rather than a plain flat dropdown. See
// ./sidebar09-12-shared.ts and ./sidebar05-08-shared.ts.

import type { DomphyElement, Listener, State } from "@domphy/core";
import { rawHtml, toState } from "@domphy/core";
import { themeColor, themeDensity, themeSpacing } from "@domphy/theme";
import { buttonGhost, icon, popover, small } from "@domphy/ui";
import {
  emojiGlyph,
  ICON_BAR_CHART,
  ICON_CALENDAR,
  ICON_CHEVRON_RIGHT,
  ICON_FILE,
  ICON_GRID,
  ICON_HOME,
  ICON_INBOX,
  ICON_LIFEBUOY,
  ICON_MORE,
  ICON_PANEL_TOGGLE,
  ICON_PLUS,
  ICON_SEARCH,
  ICON_SETTINGS,
  ICON_SPARKLE,
  ICON_TRASH,
  interactiveRowStyle,
  renderPlainNavRow,
  renderTeamSwitcher,
  type SidebarBreadcrumbItem,
  type SidebarNavMainItem,
  type SidebarTeam,
  sidebarBreadcrumb,
  sidebarIcon,
  sidebarMainContent,
  sidebarStyledPopoverContent,
  useShowMore,
  verticalDivider,
} from "./sidebar09-12-shared.js";

type Sidebar10FavoriteItem = { emoji: string; label: string; href?: string };
type Sidebar10Page = { title: string; href?: string; emoji?: string };

// Page-action icons the header's "…" menu needs that aren't in the shared set
// (lucide equivalents). Local to this block to keep the shared icon module lean.
const SVG_OPEN =
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="1em" height="1em">';
const ICON_LINK = `${SVG_OPEN}<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
const ICON_COPY = `${SVG_OPEN}<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
const ICON_MOVE_TO = `${SVG_OPEN}<polyline points="15 14 20 9 15 4"/><path d="M4 20v-7a4 4 0 0 1 4-4h12"/></svg>`;
const ICON_UNDO = `${SVG_OPEN}<polyline points="9 14 4 9 9 4"/><path d="M20 20v-7a4 4 0 0 0-4-4H4"/></svg>`;
const ICON_HISTORY = `${SVG_OPEN}<path d="M7 2h10"/><path d="M5 6h14"/><rect width="18" height="12" x="3" y="10" rx="2"/></svg>`;
const ICON_BELL = `${SVG_OPEN}<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>`;
const ICON_IMPORT = `${SVG_OPEN}<path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg>`;
const ICON_EXPORT = `${SVG_OPEN}<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>`;
const ICON_STAR = `${SVG_OPEN}<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`;
const ICON_STAR_OFF = `${SVG_OPEN}<path d="M8.34 8.34 2 9.27l5 4.87L5.82 21 12 17.77l1.28.67"/><path d="M18.42 12.76 22 9.27l-6.91-1.01L12 2l-1.44 2.91"/><line x1="2" x2="22" y1="2" y2="22"/></svg>`;
const ICON_ARROW_UP_RIGHT = `${SVG_OPEN}<path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>`;
type Sidebar10Workspace = {
  name: string;
  emoji: string;
  expanded?: boolean;
  pages: Sidebar10Page[];
};
type Sidebar10SecondaryLink = { title: string; icon: string; href?: string };

type Sidebar10Props = {
  teams?: SidebarTeam[];
  favorites?: Sidebar10FavoriteItem[];
  favoritesVisibleCount?: number;
  workspaces?: Sidebar10Workspace[];
  workspacesVisibleCount?: number;
  secondaryLinks?: Sidebar10SecondaryLink[];
  breadcrumbItems?: SidebarBreadcrumbItem[];
  children?: DomphyElement | DomphyElement[];
};

const DEFAULT_TEAMS: SidebarTeam[] = [
  { name: "Acme Inc", plan: "Enterprise" },
  { name: "Acme Corp.", plan: "Startup" },
  { name: "Evil Corp.", plan: "Free" },
];

const DEFAULT_FAVORITES: Sidebar10FavoriteItem[] = [
  { emoji: "📊", label: "Roadmap" },
  { emoji: "📝", label: "Meeting Notes" },
  { emoji: "🎯", label: "OKRs" },
  { emoji: "🐛", label: "Bug Tracker" },
  { emoji: "🚀", label: "Launch Plan" },
  { emoji: "💰", label: "Budget" },
  { emoji: "📚", label: "Handbook" },
  { emoji: "🎨", label: "Design System" },
  { emoji: "📈", label: "Analytics" },
  { emoji: "🧭", label: "Onboarding" },
  { emoji: "🗓️", label: "Sprint Calendar" },
  { emoji: "🤝", label: "Partnerships" },
  { emoji: "🔐", label: "Security" },
];

const DEFAULT_WORKSPACES: Sidebar10Workspace[] = [
  {
    name: "Engineering",
    emoji: "🛠️",
    pages: [
      { emoji: "🏛️", title: "Architecture" },
      { emoji: "📄", title: "RFCs" },
      { emoji: "📟", title: "On-call" },
    ],
  },
  {
    name: "Product",
    emoji: "📦",
    pages: [
      { emoji: "🗺️", title: "Roadmap" },
      { emoji: "💬", title: "Feedback" },
      { emoji: "📋", title: "Specs" },
    ],
  },
  {
    name: "Design",
    emoji: "🎨",
    pages: [
      { emoji: "🧩", title: "Components" },
      { emoji: "🎟️", title: "Tokens" },
    ],
  },
  {
    name: "Marketing",
    emoji: "📣",
    pages: [
      { emoji: "🚀", title: "Campaigns" },
      { emoji: "🏷️", title: "Brand" },
      { emoji: "✍️", title: "Content" },
    ],
  },
  {
    name: "Sales",
    emoji: "💼",
    pages: [
      { emoji: "📈", title: "Pipeline" },
      { emoji: "📕", title: "Playbook" },
    ],
  },
  {
    name: "Finance",
    emoji: "💰",
    pages: [
      { emoji: "🔮", title: "Forecasts" },
      { emoji: "🧾", title: "Invoices" },
    ],
  },
];

const DEFAULT_SECONDARY_LINKS: Sidebar10SecondaryLink[] = [
  { title: "Calendar", icon: ICON_CALENDAR },
  { title: "Settings", icon: ICON_SETTINGS },
  { title: "Templates", icon: ICON_GRID },
  { title: "Trash", icon: ICON_TRASH },
  { title: "Help", icon: ICON_LIFEBUOY },
];

/** Uppercase muted section heading (hidden in icon-rail mode). */
function sectionLabel(
  text: string,
  collapsed: State<boolean>,
): DomphyElement<"small"> {
  return {
    small: text,
    style: {
      display: (l: Listener) => (collapsed.get(l) ? "none" : "block"),
      paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
      paddingBlock: themeSpacing(1),
      textTransform: "uppercase",
    },
    $: [small({ color: "neutral" })],
  } as unknown as DomphyElement<"small">;
}

/** A favorite row: emoji + label + hover-revealed "more" popover trigger. */
function favoriteRow(
  item: Sidebar10FavoriteItem,
  collapsed: State<boolean>,
): DomphyElement<"li"> {
  const actionsMenu = sidebarStyledPopoverContent([
    { items: [{ icon: ICON_STAR_OFF, label: "Remove from Favorites" }] },
    {
      items: [
        { icon: ICON_LINK, label: "Copy Link" },
        { icon: ICON_ARROW_UP_RIGHT, label: "Open in New Tab" },
      ],
    },
    { items: [{ icon: ICON_TRASH, label: "Delete" }] },
  ]);

  return {
    li: [
      {
        div: [
          {
            a: [
              emojiGlyph(item.emoji),
              {
                span: item.label,
                style: { flex: "1", textAlign: "left" },
              } as unknown as DomphyElement,
            ],
            href: item.href ?? "#",
            style: {
              display: "flex",
              alignItems: "center",
              flex: "1",
              minWidth: "0",
              gap: themeSpacing(2),
              textDecoration: () => "none",
              overflow: "hidden",
              whiteSpace: "nowrap",
              color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
            },
          } as unknown as DomphyElement,
          {
            button: sidebarIcon(ICON_MORE),
            type: "button",
            dataSlot: "row-more",
            ariaLabel: `${item.label} actions`,
            style: {
              display: "none",
              flexShrink: "0",
              border: "none",
              background: "none",
              cursor: "pointer",
              color: (l: Listener) => themeColor(l, "shift-7", "neutral"),
            },
            $: [popover({ placement: "right-start", content: actionsMenu })],
          } as unknown as DomphyElement,
        ],
        style: {
          display: (l: Listener) => (collapsed.get(l) ? "none" : "flex"),
          alignItems: "center",
          width: "100%",
          gap: themeSpacing(1),
          paddingBlock: (l: Listener) => themeSpacing(themeDensity(l) * 1.5),
          paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 1),
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
          backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
          "&:hover": {
            backgroundColor: (l: Listener) =>
              themeColor(l, "shift-2", "neutral"),
          },
          "&:hover [data-slot=row-more], &:focus-within [data-slot=row-more]": {
            display: "inline-flex",
          },
        },
      } as unknown as DomphyElement,
      {
        a: [emojiGlyph(item.emoji)],
        href: item.href ?? "#",
        ariaLabel: item.label,
        style: {
          display: (l: Listener) => (collapsed.get(l) ? "flex" : "none"),
          justifyContent: "center",
          paddingBlock: (l: Listener) => themeSpacing(themeDensity(l) * 1.5),
          textDecoration: () => "none",
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
        },
      } as unknown as DomphyElement,
    ],
    _key: item.label,
  } as DomphyElement<"li">;
}

/**
 * A workspace tree node. Mirrors upstream nav-workspaces.tsx: a name **link**
 * (`<a href="#">`) as the row body, a chevron collapse toggle anchored on the
 * **left** (rotates 90deg when open), and a separate Plus action on the
 * **right**. The chevron button — not the whole row — is the toggle, so the
 * name stays an independent navigable link. All workspaces start collapsed
 * (upstream wraps each in a bare `<Collapsible>` with no `defaultOpen`).
 */
function workspaceNode(
  workspace: Sidebar10Workspace,
  collapsed: State<boolean>,
): DomphyElement<"li"> {
  const open = toState(workspace.expanded ?? false);

  return {
    li: [
      {
        div: [
          {
            button: [
              {
                span: rawHtml(ICON_CHEVRON_RIGHT),
                dataSlot: "chevron",
                style: {
                  display: "inline-flex",
                  transition: "transform 150ms ease",
                  transform: (l: Listener) =>
                    open.get(l) ? "rotate(90deg)" : "none",
                },
                $: [icon({ color: "neutral" })],
              } as unknown as DomphyElement,
            ],
            type: "button",
            dataSlot: "chevron-toggle",
            ariaLabel: `Toggle ${workspace.name}`,
            ariaExpanded: (l: Listener) => (open.get(l) ? "true" : "false"),
            onClick: (event: Event) => {
              event.preventDefault();
              open.set(!open.get());
            },
            style: {
              display: (l: Listener) => (open.get(l) ? "inline-flex" : "none"),
              flexShrink: "0",
              alignItems: "center",
              justifyContent: "center",
              border: "none",
              background: "none",
              cursor: "pointer",
              padding: "0",
              color: (l: Listener) => themeColor(l, "shift-7", "neutral"),
            },
          } as unknown as DomphyElement,
          {
            a: [
              emojiGlyph(workspace.emoji),
              {
                span: workspace.name,
                style: { flex: "1", textAlign: "left" },
              } as unknown as DomphyElement,
            ],
            href: "#",
            style: {
              display: "flex",
              alignItems: "center",
              flex: "1",
              minWidth: "0",
              gap: themeSpacing(2),
              textDecoration: () => "none",
              overflow: "hidden",
              whiteSpace: "nowrap",
              color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
            },
          } as unknown as DomphyElement,
          {
            button: sidebarIcon(ICON_PLUS),
            type: "button",
            dataSlot: "row-add",
            ariaLabel: `Add page to ${workspace.name}`,
            onClick: (event: Event) => {
              event.preventDefault();
              event.stopPropagation();
            },
            style: {
              display: "none",
              flexShrink: "0",
              border: "none",
              background: "none",
              cursor: "pointer",
              color: (l: Listener) => themeColor(l, "shift-7", "neutral"),
            },
          } as unknown as DomphyElement,
        ],
        style: {
          display: "flex",
          alignItems: "center",
          gap: themeSpacing(2),
          width: "100%",
          paddingBlock: (l: Listener) => themeSpacing(themeDensity(l) * 1.5),
          paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 1),
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
          backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
          "&:hover": {
            backgroundColor: (l: Listener) =>
              themeColor(l, "shift-2", "neutral"),
          },
          "&:hover [data-slot=chevron-toggle], &:focus-within [data-slot=chevron-toggle]":
            { display: "inline-flex" },
          "&:hover [data-slot=row-add], &:focus-within [data-slot=row-add]": {
            display: "inline-flex",
          },
        },
      } as unknown as DomphyElement,
      {
        ul: workspace.pages.map((page, index) => ({
          li: [
            {
              a: [
                ...(page.emoji ? [emojiGlyph(page.emoji)] : []),
                {
                  span: page.title,
                  style: { flex: "1", textAlign: "left" },
                } as unknown as DomphyElement,
              ],
              href: page.href ?? "#",
              style: {
                display: "flex",
                alignItems: "center",
                gap: themeSpacing(2),
                paddingBlock: (l: Listener) =>
                  themeSpacing(themeDensity(l) * 1.5),
                paddingInline: (l: Listener) =>
                  themeSpacing(themeDensity(l) * 3),
                borderRadius: (l: Listener) =>
                  themeSpacing(themeDensity(l) * 1),
                textDecoration: () => "none",
                color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
                backgroundColor: (l: Listener) =>
                  themeColor(l, "inherit", "neutral"),
                "&:hover": {
                  backgroundColor: (l: Listener) =>
                    themeColor(l, "shift-2", "neutral"),
                },
              },
            } as unknown as DomphyElement,
          ],
          _key: index,
        })) as unknown as DomphyElement[],
        style: {
          listStyle: "none",
          margin: "0",
          display: "flex",
          flexDirection: "column",
          gap: themeSpacing(0.5),
          marginInlineStart: themeSpacing(5),
          paddingInlineStart: themeSpacing(3),
          paddingInlineEnd: "0",
          borderInlineStart: (l: Listener) =>
            `1px solid ${themeColor(l, "shift-3", "neutral")}`,
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
          overflow: "hidden",
          maxHeight: (l: Listener) => (open.get(l) ? themeSpacing(240) : "0"),
          opacity: (l: Listener) => (open.get(l) ? "1" : "0"),
          paddingBlock: (l: Listener) => (open.get(l) ? themeSpacing(1) : "0"),
          transition: "max-height 180ms linear, opacity 180ms linear",
        },
      } as unknown as DomphyElement,
    ],
    _key: workspace.name,
    style: { display: (l: Listener) => (collapsed.get(l) ? "none" : "block") },
  } as unknown as DomphyElement<"li">;
}

/** A real "show more" toggle row — reveals the rest of an overflowed list. */
function moreRow(
  label: string,
  onClick: () => void,
  collapsed: State<boolean>,
): DomphyElement<"li"> {
  return {
    li: [
      {
        button: [
          sidebarIcon(ICON_MORE),
          {
            span: label,
            style: { flex: "1", textAlign: "left" },
          } as unknown as DomphyElement,
        ],
        type: "button",
        onClick,
        style: {
          ...interactiveRowStyle(true),
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
        },
      } as unknown as DomphyElement,
    ],
    _key: "show-more",
    style: { display: (l: Listener) => (collapsed.get(l) ? "none" : "block") },
  } as unknown as DomphyElement<"li">;
}

/**
 * A quick-link nav row, optionally carrying a trailing count badge. The badge
 * uses renderPlainNavRow()'s built-in in-flow pill (upstream SidebarMenuBadge
 * sits inside the row, ml-auto) — an earlier absolutely-positioned overlay
 * badge rendered clipped at the sidebar's padding edge.
 */
function quickLinkRow(
  item: SidebarNavMainItem & { badge?: string },
  collapsed: State<boolean>,
): DomphyElement<"li"> {
  return renderPlainNavRow(item, collapsed);
}

/**
 * Centered muted placeholder box for the main content — mirrors upstream
 * page.tsx (`mx-auto max-w-3xl rounded-xl bg-muted/50`). `fill` = the second,
 * height-filling box (`h-full`); otherwise a fixed `h-24` box.
 */
function centeredBox(fill: boolean): DomphyElement<"div"> {
  return {
    div: null,
    // Near-white muted fill (upstream bg-muted/50); shift-2 reads dirty here.
    dataTone: "shift-1",
    style: {
      marginInline: "auto",
      width: "100%",
      maxWidth: themeSpacing(192),
      ...(fill
        ? { flex: "1", minHeight: themeSpacing(24) }
        : { height: themeSpacing(24) }),
      borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 2),
      backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
    },
  } as unknown as DomphyElement<"div">;
}

function mainHeader(props: {
  onToggle: () => void;
  breadcrumbItems: SidebarBreadcrumbItem[];
}): DomphyElement<"header"> {
  // Notion-style page-actions menu — matches upstream nav-actions.tsx's four
  // grouped sections.
  const actionsMenu = sidebarStyledPopoverContent([
    {
      items: [
        { icon: ICON_SETTINGS, label: "Customize Page" },
        { icon: ICON_FILE, label: "Turn into wiki" },
      ],
    },
    {
      items: [
        { icon: ICON_LINK, label: "Copy Link" },
        { icon: ICON_COPY, label: "Duplicate" },
        { icon: ICON_MOVE_TO, label: "Move to" },
        { icon: ICON_TRASH, label: "Move to Trash" },
      ],
    },
    {
      items: [
        { icon: ICON_UNDO, label: "Undo" },
        { icon: ICON_BAR_CHART, label: "View analytics" },
        { icon: ICON_HISTORY, label: "Version History" },
        { icon: ICON_TRASH, label: "Show delete pages" },
        { icon: ICON_BELL, label: "Notifications" },
      ],
    },
    {
      items: [
        { icon: ICON_IMPORT, label: "Import" },
        { icon: ICON_EXPORT, label: "Export" },
      ],
    },
  ]);

  return {
    header: [
      {
        button: [sidebarIcon(ICON_PANEL_TOGGLE)],
        type: "button",
        ariaLabel: "Toggle sidebar",
        onClick: props.onToggle,
        $: [buttonGhost({ color: "neutral" })],
      } as unknown as DomphyElement,
      verticalDivider(),
      sidebarBreadcrumb(props.breadcrumbItems),
      {
        div: [
          {
            small: "Edit Oct 08",
            // Upstream: `hidden ... md:inline-block` — hidden below the md breakpoint.
            style: {
              display: "none",
              "@media (min-width: 768px)": { display: "inline-block" },
            },
            $: [small({ color: "neutral" })],
          } as unknown as DomphyElement,
          {
            button: sidebarIcon(ICON_STAR),
            type: "button",
            ariaLabel: "Favorite this page",
            style: {
              border: "none",
              background: "none",
              cursor: "pointer",
              color: (l: Listener) => themeColor(l, "shift-7", "neutral"),
            },
          } as unknown as DomphyElement,
          {
            button: sidebarIcon(ICON_MORE),
            type: "button",
            ariaLabel: "More actions",
            // Upstream NavActions opens the page-actions popover on mount
            // (`useEffect(() => setIsOpen(true), [])`) — an idempotent *set*,
            // not a toggle. Domphy's popover only inserts its floating content
            // on `show()`, reachable here only through the trigger's click
            // handler, so replicate the post-mount effect with a synthetic
            // click. Fire it synchronously (not `setTimeout(fn, 0)`): by the
            // time `_onMount` runs the click handler is already wired, and a
            // deferred macrotask here would queue on every render even when
            // nothing else interacts with this button — those pile up across
            // renders and, once flushed together, each extra `show()` call
            // resets the popover's open debounce, delaying the real open past
            // any caller's wait window. Guard on `aria-expanded` so this is a
            // no-op (not a toggle-closed) if the popover is already open.
            _onMount: (node: { domElement: HTMLElement | null }) => {
              if (node.domElement?.getAttribute("aria-expanded") !== "true") {
                node.domElement?.click();
              }
            },
            style: {
              border: "none",
              background: "none",
              cursor: "pointer",
              color: (l: Listener) => themeColor(l, "shift-7", "neutral"),
            },
            $: [popover({ placement: "bottom-end", content: actionsMenu })],
          } as unknown as DomphyElement,
        ],
        style: {
          marginInlineStart: "auto",
          display: "flex",
          alignItems: "center",
          gap: themeSpacing(3),
        },
      } as unknown as DomphyElement,
    ],
    style: {
      position: "sticky",
      top: "0",
      zIndex: "10",
      display: "flex",
      alignItems: "center",
      gap: (l: Listener) => themeSpacing(themeDensity(l) * 3),
      height: themeSpacing(14),
      flexShrink: "0",
      paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 4),
      borderBottom: (l: Listener) =>
        `1px solid ${themeColor(l, "shift-3", "neutral")}`,
      backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
    },
  } as unknown as DomphyElement<"header">;
}

/**
 * Persistent workspace sidebar (team switcher, favorites, nested workspace
 * tree, secondary links) with a "sidebar in a popover" quick-actions menu.
 * Call with no arguments for a fully working demo.
 */
function sidebar10(props: Sidebar10Props = {}): DomphyElement<"div"> {
  const {
    teams = DEFAULT_TEAMS,
    favorites = DEFAULT_FAVORITES,
    favoritesVisibleCount = 10,
    workspaces = DEFAULT_WORKSPACES,
    workspacesVisibleCount = 5,
    secondaryLinks = DEFAULT_SECONDARY_LINKS,
    breadcrumbItems = [{ label: "Project Management & Task Tracking" }],
    children,
  } = props;

  const collapsed = toState(false);
  const favoritesShowMore = useShowMore(favorites, favoritesVisibleCount);
  const workspacesShowMore = useShowMore(workspaces, workspacesVisibleCount);

  const quickLinks: Array<SidebarNavMainItem & { badge?: string }> = [
    { title: "Search", icon: ICON_SEARCH, href: "#" },
    { title: "Ask AI", icon: ICON_SPARKLE, href: "#" },
    { title: "Home", icon: ICON_HOME, href: "#", active: true },
    { title: "Inbox", icon: ICON_INBOX, href: "#", badge: "10" },
  ];

  const asideElement: DomphyElement<"aside"> = {
    aside: [
      renderTeamSwitcher(teams),
      {
        ul: quickLinks.map((item) => quickLinkRow(item, collapsed)),
        style: {
          listStyle: "none",
          margin: "0",
          padding: "0",
          display: "flex",
          flexDirection: "column",
          gap: themeSpacing(0.5),
          paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
        },
      } as unknown as DomphyElement,
      {
        nav: [
          {
            div: [
              sectionLabel("Favorites", collapsed),
              {
                ul: (listener: Listener) => {
                  const rows: DomphyElement[] = favoritesShowMore
                    .slice(listener)
                    .map((item) => favoriteRow(item, collapsed));
                  if (
                    !favoritesShowMore.visible.get(listener) &&
                    favorites.length > favoritesVisibleCount
                  ) {
                    rows.push(
                      moreRow(
                        "More",
                        () => favoritesShowMore.visible.set(true),
                        collapsed,
                      ),
                    );
                  }
                  return rows;
                },
                style: {
                  listStyle: "none",
                  margin: "0",
                  padding: "0",
                  display: "flex",
                  flexDirection: "column",
                  gap: themeSpacing(0.5),
                },
              } as unknown as DomphyElement,
            ],
            style: {
              display: "flex",
              flexDirection: "column",
              gap: themeSpacing(1),
              marginTop: themeSpacing(3),
            },
          } as unknown as DomphyElement,
          {
            div: [
              sectionLabel("Workspaces", collapsed),
              {
                ul: (listener: Listener) => {
                  const rows: DomphyElement[] = workspacesShowMore
                    .slice(listener)
                    .map((workspace) => workspaceNode(workspace, collapsed));
                  if (
                    !workspacesShowMore.visible.get(listener) &&
                    workspaces.length > workspacesVisibleCount
                  ) {
                    rows.push(
                      moreRow(
                        "More",
                        () => workspacesShowMore.visible.set(true),
                        collapsed,
                      ),
                    );
                  }
                  return rows;
                },
                style: {
                  listStyle: "none",
                  margin: "0",
                  padding: "0",
                  display: "flex",
                  flexDirection: "column",
                  gap: themeSpacing(0.5),
                },
              } as unknown as DomphyElement,
            ],
            style: {
              display: "flex",
              flexDirection: "column",
              gap: themeSpacing(1),
              marginTop: themeSpacing(3),
            },
          } as unknown as DomphyElement,
        ],
        style: {
          flex: "1",
          minHeight: "0",
          overflowY: "auto",
          overflowX: "hidden",
          paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          // Clearance above the pinned secondary nav so the last scrollable
          // row never reads as clipped against its top border.
          paddingBlockEnd: (l: Listener) => themeSpacing(themeDensity(l) * 2),
        },
      } as unknown as DomphyElement,
      {
        ul: secondaryLinks.map((link) =>
          renderPlainNavRow(
            { title: link.title, icon: link.icon, href: link.href },
            collapsed,
          ),
        ),
        style: {
          listStyle: "none",
          margin: "0",
          padding: (l: Listener) => `0 ${themeSpacing(themeDensity(l) * 3)}`,
          display: "flex",
          flexDirection: "column",
          gap: themeSpacing(0.5),
          flexShrink: "0",
          borderTop: (l: Listener) =>
            `1px solid ${themeColor(l, "shift-3", "neutral")}`,
          paddingTop: (l: Listener) => themeSpacing(themeDensity(l) * 2),
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
        },
      } as unknown as DomphyElement,
      // Upstream app-sidebar.tsx renders <SidebarRail /> — a thin always-present
      // edge strip that toggles the sidebar on click (same pattern as sidebar11).
      {
        div: null,
        ariaHidden: "true",
        onClick: () => collapsed.set(!collapsed.get()),
        style: {
          position: "absolute",
          insetBlock: "0",
          insetInlineEnd: "0",
          width: themeSpacing(1),
          cursor: "col-resize",
        },
      } as unknown as DomphyElement,
    ],
    style: {
      position: "relative",
      display: "flex",
      flexDirection: "column",
      flexShrink: "0",
      width: (l: Listener) =>
        collapsed.get(l) ? themeSpacing(12) : themeSpacing(64),
      overflow: "hidden",
      transition: "width 0.2s linear",
      borderInlineEnd: (l: Listener) =>
        `1px solid ${themeColor(l, "shift-3", "neutral")}`,
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
      backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
    },
  } as unknown as DomphyElement<"aside">;

  const mainElement: DomphyElement<"main"> = {
    main: [
      mainHeader({
        onToggle: () => collapsed.set(!collapsed.get()),
        breadcrumbItems,
      }),
      sidebarMainContent(children ?? [centeredBox(false), centeredBox(true)]),
    ],
    style: {
      display: "flex",
      flexDirection: "column",
      flex: "1",
      minWidth: "0",
      minHeight: "0",
      overflow: "hidden",
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
      backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
    },
  } as unknown as DomphyElement<"main">;

  return {
    div: [asideElement, mainElement],
    dataTone: "shift-0",
    style: {
      display: "flex",
      height: "100dvh",
      overflow: "hidden",
      position: "relative",
      backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
    },
  } as unknown as DomphyElement<"div">;
}

export { sidebar10 };
export type {
  Sidebar10FavoriteItem,
  Sidebar10Page,
  Sidebar10Props,
  Sidebar10SecondaryLink,
  Sidebar10Workspace,
};

← Back to shadcn/ui catalog