Domphy

sidebarLeftRight

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

Implementation notes

Self-contained dual-sidebar Notion-like workspace (left: org switcher + quick links + emoji favorites w/ hover more-menu + expandable emoji workspace groups + utility footer; right: account header + inline month calendar + checkbox calendar-visibility groups + New Calendar action). Reuses only stable, already-exported read-only pieces from sidebar05-08-shared.ts (renderTeamSwitcher/icons/sidebarBackdrop/etc.) for the org switcher, deliberately avoiding edits to that shared module since it was being actively modified by a concurrent process mid-session (observed content changes between two reads of the same file during this run) — editing it to add new exports would have risked a lost-update race, so a couple of small renderer functions are self-contained here instead. Left-sidebar collapse uses the lighter 'fade the label, keep the icon/emoji' CSS technique (matching the sidebar01-04 family's collapsibleLabel pattern) rather than the heavier dual-row+tooltip pattern used by sidebar07/08, since favorites+workspaces here total 15+ rows and duplicating every one would roughly double the file for no functional difference — a scope choice, not a functional gap. The right sidebar's month calendar is a hand-rolled, always-visible inline grid (own date math: month nav, weekday header, 6x7 day grid keyed by ISO date) rather than @domphy/ui's datePicker() patch, because that patch is architecturally an input-triggered floating popover, not a standalone inline grid; the selected-day cell uses a fixed (non-'inherit') accent background by design, the same accepted exception the core datePicker() patch itself makes for its own selected-day cell (annotated with _doctorDisable and explained inline). Right sidebar is breakpoint-driven display/flex (no animated drawer, no manual toggle) per spec — it 'stays visible on desktop.'

Status: ported · Reference: shadcn/ui original

// shadcn/ui "sidebar-left-right" block — clean-room reimplementation from the
// public behavior/visual spec only (no upstream source viewed). A
// Notion-like three-column workspace: a primary left sidebar (org switcher,
// quick links, emoji-prefixed favorites, expandable emoji-prefixed workspace
// pages, pinned utility footer) and a secondary right sidebar dedicated to an
// inline month calendar + togglable calendar-visibility list, sandwiching a
// scrollable main column. Built from two independent sidebar instances (one
// mirrored) around a shared main area — not an exotic new primitive.
//
// Implemented purely from the block's public functional/visual spec — no
// upstream shadcn/ui source was viewed or copied.

import type { DomphyElement, ElementNode, Listener, ReadableState, State } from "@domphy/core";
import { RecordState, toState } from "@domphy/core";
import { avatar, breadcrumb, buttonGhost, icon, inputCheckbox, menu, popover, small, strong, tooltip } from "@domphy/ui";
import { type ThemeColor, themeColor, themeDensity, themeSpacing } from "@domphy/theme";
import {
  ICON_CHEVRONS_UPDOWN,
  ICON_CHEVRON_RIGHT,
  ICON_MORE,
  ICON_PANEL_TOGGLE,
  ICON_PLUS,
  ICON_SEARCH,
  renderTeamSwitcher,
  sidebarBackdrop,
  sidebarIcon,
  sidebarMainContent,
  verticalDivider,
  type SidebarTeam,
} from "./sidebar05-08-shared.js";

// ---------------------------------------------------------------------------
// Hand-authored generic line icons (24x24, stroke=currentColor) — simple
// geometric shapes, not sourced from any icon library.
// ---------------------------------------------------------------------------

const ICON_SPARKLE =
  '<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"><path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z"/></svg>';

const ICON_HOME =
  '<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"><path d="M4 11l8-7 8 7"/><path d="M6 10v9a1 1 0 0 0 1 1h4v-6h2v6h4a1 1 0 0 0 1-1v-9"/></svg>';

const ICON_INBOX =
  '<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"><path d="M3 12h4l2 3h6l2-3h4"/><path d="M5.5 5h13l2.5 7v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-7z"/></svg>';

const ICON_CALENDAR =
  '<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"><rect x="4" y="6" width="16" height="14" rx="2"/><path d="M4 10h16M8 4v4M16 4v4"/></svg>';

const ICON_SETTINGS =
  '<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"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="8"/></svg>';

const ICON_TEMPLATE =
  '<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"><rect x="4" y="3" width="16" height="18" rx="1"/><path d="M8 8h8M8 12h8M8 16h5"/></svg>';

const ICON_TRASH =
  '<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"><path d="M5 7h14M9 7V4h6v3M6 7l1 13h10l1-13"/></svg>';

const ICON_HELP =
  '<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"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 0 1 5 0c0 1.7-2.5 2-2.5 3.5"/><path d="M12 17h.01"/></svg>';

// ---------------------------------------------------------------------------
// Data shapes
// ---------------------------------------------------------------------------

type FavoriteItem = { id: string; label: string; emoji: string; href?: string };
type WorkspacePage = { label: string; emoji: string; href?: string };
type WorkspaceGroup = { id: string; label: string; emoji: string; pages: WorkspacePage[] };
type FooterLink = { label: string; icon: string; href?: string };
type CurrentUser = { name: string; email: string; avatarUrl?: string };
type CalendarEntry = { id: string; name: string; color: ThemeColor; checked?: boolean };
type CalendarGroup = { label: string; entries: CalendarEntry[] };

type SidebarLeftRightProps = {
  organizations?: SidebarTeam[];
  favorites?: FavoriteItem[];
  workspaces?: WorkspaceGroup[];
  footerLinks?: FooterLink[];
  user?: CurrentUser;
  calendarGroups?: CalendarGroup[];
  /** Fixed reference date for the inline month calendar. */
  selectedDate?: Date;
  breadcrumbLabel?: string;
  defaultLeftCollapsed?: boolean;
  children?: DomphyElement | DomphyElement[];
};

const DEFAULT_ORGANIZATIONS: SidebarTeam[] = [
  { name: "Acme Inc", plan: "Enterprise" },
  { name: "Acme Corp", plan: "Startup" },
];

const DEFAULT_FAVORITES: FavoriteItem[] = [
  { id: "getting-started", label: "Getting Started", emoji: "🏠" },
  { id: "roadmap", label: "Roadmap", emoji: "📊" },
  { id: "ideas", label: "Ideas", emoji: "💡" },
  { id: "meeting-notes", label: "Meeting Notes", emoji: "📝" },
  { id: "okrs", label: "OKRs", emoji: "🎯" },
  { id: "wiki", label: "Wiki", emoji: "📚" },
  { id: "bug-tracker", label: "Bug Tracker", emoji: "🐛" },
  { id: "launch-plan", label: "Launch Plan", emoji: "🚀" },
  { id: "budget", label: "Budget", emoji: "💰" },
  { id: "team-directory", label: "Team Directory", emoji: "👥" },
];

const DEFAULT_WORKSPACES: WorkspaceGroup[] = [
  {
    id: "product",
    label: "Product",
    emoji: "📁",
    pages: [
      { label: "Roadmap", emoji: "🗺️" },
      { label: "Specs", emoji: "📐" },
      { label: "Feedback", emoji: "💬" },
    ],
  },
  {
    id: "engineering",
    label: "Engineering",
    emoji: "📁",
    pages: [
      { label: "Architecture", emoji: "🏗️" },
      { label: "Runbooks", emoji: "📖" },
    ],
  },
  {
    id: "marketing",
    label: "Marketing",
    emoji: "📁",
    pages: [
      { label: "Campaigns", emoji: "📣" },
      { label: "Brand Assets", emoji: "🎨" },
    ],
  },
  {
    id: "sales",
    label: "Sales",
    emoji: "📁",
    pages: [
      { label: "Playbook", emoji: "📔" },
      { label: "Proposals", emoji: "📄" },
    ],
  },
  {
    id: "design",
    label: "Design",
    emoji: "📁",
    pages: [
      { label: "Design System", emoji: "🧩" },
      { label: "Prototypes", emoji: "🖼️" },
    ],
  },
];

const DEFAULT_FOOTER_LINKS: FooterLink[] = [
  { label: "Calendar", icon: ICON_CALENDAR },
  { label: "Settings", icon: ICON_SETTINGS },
  { label: "Templates", icon: ICON_TEMPLATE },
  { label: "Trash", icon: ICON_TRASH },
  { label: "Help", icon: ICON_HELP },
];

const DEFAULT_USER: CurrentUser = { name: "Shad Cn", email: "shadcn@example.com" };

const DEFAULT_CALENDAR_GROUPS: CalendarGroup[] = [
  {
    label: "My Calendars",
    entries: [
      { id: "personal", name: "Personal", color: "primary", checked: true },
      { id: "work", name: "Work", color: "secondary", checked: true },
      { id: "family", name: "Family", color: "success", checked: true },
    ],
  },
  {
    label: "Favorites",
    entries: [
      { id: "holidays", name: "Holidays", color: "warning", checked: true },
      { id: "birthdays", name: "Birthdays", color: "error", checked: false },
    ],
  },
  {
    label: "Other",
    entries: [
      { id: "travel", name: "Travel", color: "info", checked: false },
      { id: "reminders", name: "Reminders", color: "neutral", checked: true },
      { id: "deadlines", name: "Deadlines", color: "error", checked: true },
    ],
  },
];

// Fixed reference date (not "today") so the zero-arg demo is deterministic.
const DEFAULT_SELECTED_DATE = new Date(2024, 9, 15);
const WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];

// ---------------------------------------------------------------------------
// Date helpers (no third-party library)
// ---------------------------------------------------------------------------

function startOfMonth(date: Date): Date {
  return new Date(date.getFullYear(), date.getMonth(), 1);
}
function addMonths(date: Date, count: number): Date {
  return new Date(date.getFullYear(), date.getMonth() + count, 1);
}
function addDays(date: Date, count: number): Date {
  return new Date(date.getFullYear(), date.getMonth(), date.getDate() + count);
}
function sameDay(a: Date, b: Date): boolean {
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}
function isoOf(date: Date): string {
  return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
}

// ---------------------------------------------------------------------------
// Left sidebar — text fades away on collapse (icon/emoji always stays), one
// row definition serving both expanded and icon-rail states. Lighter-weight
// than the dual-row + tooltip pattern used elsewhere in the family: this
// variant's favorites/workspace lists are long enough that duplicating every
// row would roughly double this file for little visual benefit.
// ---------------------------------------------------------------------------

/** Row label that fades/collapses away in icon-rail mode. */
function collapsibleLabel(collapsed: ReadableState<boolean>, content: string): DomphyElement<"span"> {
  return {
    span: content,
    style: {
      display: "inline-flex",
      overflow: "hidden",
      whiteSpace: "nowrap",
      flex: "1",
      textAlign: "left",
      opacity: (l: Listener) => (collapsed.get(l) ? 0 : 1),
      maxWidth: (l: Listener) => (collapsed.get(l) ? "0em" : themeSpacing(56)),
      transition: "opacity 150ms linear, max-width 150ms linear",
    },
  } as unknown as DomphyElement<"span">;
}

/** Uppercase muted section heading, hidden in icon-rail mode. */
function sectionLabel(collapsed: ReadableState<boolean>, text: string): DomphyElement<"small"> {
  return {
    small: text,
    $: [small({ color: "neutral" })],
    style: {
      textTransform: "uppercase",
      paddingInline: themeSpacing(3),
      overflow: "hidden",
      whiteSpace: "nowrap",
      opacity: (l: Listener) => (collapsed.get(l) ? 0 : 1),
      maxHeight: (l: Listener) => (collapsed.get(l) ? "0em" : themeSpacing(6)),
      transition: "opacity 150ms linear, max-height 150ms linear",
    },
  } as unknown as DomphyElement<"small">;
}

/** A plain icon+label quick-access row (Search / Ask AI / Home / Inbox). */
function quickLinkRow(
  emojiOrIcon: string,
  label: string,
  collapsed: ReadableState<boolean>,
  options: { href?: string; onClick?: () => void; badge?: number } = {},
): DomphyElement<"li"> {
  const badgeElement = options.badge
    ? ({
        span: String(options.badge),
        dataTone: "shift-0",
        style: {
          display: "inline-flex",
          alignItems: "center",
          justifyContent: "center",
          minWidth: themeSpacing(5),
          paddingInline: themeSpacing(1.5),
          borderRadius: themeSpacing(999),
          opacity: (l: Listener) => (collapsed.get(l) ? 0 : 1),
          backgroundColor: (l: Listener) => themeColor(l, "inherit", "primary"),
          color: (l: Listener) => themeColor(l, "shift-10", "primary"),
        },
      } as unknown as DomphyElement)
    : null;

  const rowStyle = {
    display: "flex",
    alignItems: "center",
    width: "100%",
    gap: (l: Listener) => themeSpacing(themeDensity(l) * 2),
    paddingBlock: (l: Listener) => themeSpacing(themeDensity(l) * 1.5),
    paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
    borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 1),
    border: "none",
    cursor: "pointer",
    overflow: "hidden",
    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") },
  };

  const content = [sidebarIcon(emojiOrIcon), collapsibleLabel(collapsed, label), ...(badgeElement ? [badgeElement] : [])];

  return {
    li: [
      options.href
        ? ({ a: content, href: options.href, style: rowStyle } as unknown as DomphyElement)
        : ({ button: content, type: "button", onClick: options.onClick, style: rowStyle } as unknown as DomphyElement),
    ],
    _key: label,
  } as DomphyElement<"li">;
}

/** Favorites row: emoji + label, hover-revealed "more" icon-button with a
 * remove/copy/open/delete dropdown. */
function favoriteRow(item: FavoriteItem, collapsed: ReadableState<boolean>): DomphyElement<"li"> {
  const moreOpen = toState(false);
  const moreMenu: DomphyElement<"div"> = {
    div: null,
    style: { minWidth: themeSpacing(44) },
    $: [
      menu({
        items: [
          { label: "Remove from Favorites" },
          { label: "Copy Link" },
          { label: "Open in New Tab" },
          { label: "Delete" },
        ],
      }),
    ],
  } as unknown as DomphyElement<"div">;

  return {
    li: [
      {
        div: [
          {
            a: [sidebarIcon(item.emoji), collapsibleLabel(collapsed, item.label)],
            href: item.href ?? "#",
            style: {
              display: "flex",
              alignItems: "center",
              flex: "1",
              minWidth: "0",
              gap: (l: Listener) => themeSpacing(themeDensity(l) * 2),
              textDecoration: () => "none",
              overflow: "hidden",
              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: (l: Listener) => (moreOpen.get(l) || collapsed.get(l) ? "none" : "inline-flex"),
              flexShrink: "0",
              border: "none",
              background: "none",
              cursor: "pointer",
              color: (l: Listener) => themeColor(l, "shift-7", "neutral"),
            },
            $: [popover({ open: moreOpen, placement: "right-start", content: moreMenu })],
          } as unknown as DomphyElement,
        ],
        style: {
          display: "flex",
          alignItems: "center",
          width: "100%",
          gap: (l: Listener) => themeSpacing(themeDensity(l) * 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: (l: Listener) => (collapsed.get(l) ? "none" : "inline-flex"),
          },
        },
      } as unknown as DomphyElement,
    ],
    _key: item.id,
  } as DomphyElement<"li">;
}

/** An expandable workspace group: emoji + label + chevron, revealing a
 * nested list of emoji-prefixed pages on click. */
function workspaceGroupRow(group: WorkspaceGroup, collapsed: ReadableState<boolean>): DomphyElement<"li"> {
  return {
    li: [
      {
        details: [
          {
            summary: [
              sidebarIcon(group.emoji),
              collapsibleLabel(collapsed, group.label),
              {
                span: ICON_CHEVRON_RIGHT,
                style: {
                  transition: "transform 150ms ease",
                  display: (l: Listener) => (collapsed.get(l) ? "none" : "inline-flex"),
                },
                $: [icon({ color: "neutral" })],
              } as unknown as DomphyElement,
            ],
            style: {
              listStyle: "none",
              cursor: "pointer",
              userSelect: "none",
              display: "flex",
              alignItems: "center",
              gap: (l: Listener) => themeSpacing(themeDensity(l) * 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"),
              "&::-webkit-details-marker": { display: "none" },
              "&::marker": { content: `""` },
              "&:hover": { backgroundColor: (l: Listener) => themeColor(l, "shift-2", "neutral") },
            },
          } as unknown as DomphyElement,
          {
            ul: group.pages.map((page, index) => ({
              li: [
                {
                  a: [sidebarIcon(page.emoji), { span: page.label, style: { flex: "1", textAlign: "left" } }],
                  href: page.href ?? "#",
                  style: {
                    display: "flex",
                    alignItems: "center",
                    gap: (l: Listener) => themeSpacing(themeDensity(l) * 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"),
                    "&:hover": { backgroundColor: (l: Listener) => themeColor(l, "shift-2", "neutral") },
                  },
                } as unknown as DomphyElement,
              ],
              _key: `${group.id}-${index}`,
            })) as unknown as DomphyElement[],
            style: {
              listStyle: "none",
              margin: "0",
              display: "flex",
              flexDirection: "column",
              gap: themeSpacing(0.5),
              marginInlineStart: themeSpacing(5),
              paddingInlineStart: themeSpacing(3),
              paddingBlock: "0",
              paddingInlineEnd: "0",
              borderInlineStart: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
              color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
            },
          } as unknown as DomphyElement,
        ],
        style: {
          display: (l: Listener) => (collapsed.get(l) ? "none" : "block"),
          "&[open] summary span": { transform: "rotate(90deg)" },
        },
      } as unknown as DomphyElement,
    ],
    _key: group.id,
  } as DomphyElement<"li">;
}

/** Footer utility link (Calendar/Settings/Templates/Trash/Help). */
function footerLinkRow(item: FooterLink, collapsed: ReadableState<boolean>): DomphyElement<"li"> {
  return quickLinkRow(item.icon, item.label, collapsed, { href: item.href ?? "#" });
}

// ---------------------------------------------------------------------------
// Right sidebar — user header + inline month calendar + calendar list
// ---------------------------------------------------------------------------

function currentUserHeader(user: CurrentUser): DomphyElement<"div"> {
  const accountMenu: DomphyElement<"div"> = {
    div: null,
    style: { minWidth: themeSpacing(44) },
    $: [menu({ items: [{ label: "Account" }, { label: "Billing" }, { label: "Log out" }] })],
  } as unknown as DomphyElement<"div">;

  const avatarChild: DomphyElement<"span"> = user.avatarUrl
    ? ({ span: [{ img: null, src: user.avatarUrl, alt: user.name } as unknown as DomphyElement], $: [avatar({ color: "primary" })] } as unknown as DomphyElement<"span">)
    : ({ span: user.name.slice(0, 1).toUpperCase(), $: [avatar({ color: "primary" })] } as unknown as DomphyElement<"span">);

  return {
    div: [
      {
        button: [
          avatarChild,
          {
            div: [
              { strong: user.name, $: [strong({ color: "neutral" })] } as unknown as DomphyElement,
              { small: user.email, $: [small({ color: "neutral" })] } as unknown as DomphyElement,
            ],
            style: { display: "flex", flexDirection: "column", gap: themeSpacing(0.5), minWidth: "0", overflow: "hidden" },
          } as unknown as DomphyElement,
        ],
        type: "button",
        ariaLabel: "Account menu",
        style: {
          display: "flex",
          alignItems: "center",
          width: "100%",
          gap: (l: Listener) => themeSpacing(themeDensity(l) * 2),
          padding: (l: Listener) => themeSpacing(themeDensity(l) * 2),
          borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 1),
          border: "none",
          cursor: "pointer",
          overflow: "hidden",
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
          backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
          "&:hover": { backgroundColor: (l: Listener) => themeColor(l, "shift-2", "neutral") },
        },
        $: [popover({ placement: "bottom", content: accountMenu })],
      } as unknown as DomphyElement,
    ],
    style: {
      padding: (l: Listener) => themeSpacing(themeDensity(l) * 2),
      borderBottom: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
    },
  } as unknown as DomphyElement<"div">;
}

function calendarNavButtonStyle() {
  return {
    appearance: "none" as const,
    border: "none",
    background: "none",
    cursor: "pointer",
    width: themeSpacing(7),
    height: themeSpacing(7),
    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-3", "neutral") },
  };
}

function calendarGridRowStyle() {
  return { display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: themeSpacing(0.5) };
}

/** Compact always-visible month-grid calendar (Sunday-first, no popover),
 * styled with the primary accent color on the selected day. */
function inlineMonthCalendar(viewMonth: State<Date>, selectedDate: State<Date>): DomphyElement<"div"> {
  const monthFormatter = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" });

  const header: DomphyElement<"div"> = {
    div: [
      {
        button: "‹",
        type: "button",
        ariaLabel: "Previous month",
        onClick: () => viewMonth.set(addMonths(viewMonth.get(), -1)),
        style: calendarNavButtonStyle(),
      } as unknown as DomphyElement,
      {
        strong: (l: Listener) => monthFormatter.format(viewMonth.get(l)),
        ariaLive: "polite",
        style: { flex: "1", textAlign: "center" },
        $: [strong({ color: "neutral" })],
      } as unknown as DomphyElement,
      {
        button: "›",
        type: "button",
        ariaLabel: "Next month",
        onClick: () => viewMonth.set(addMonths(viewMonth.get(), 1)),
        style: calendarNavButtonStyle(),
      } as unknown as DomphyElement,
    ],
    style: { display: "flex", alignItems: "center", gap: themeSpacing(1), marginBottom: themeSpacing(2) },
  } as unknown as DomphyElement<"div">;

  const weekdayHeader: DomphyElement<"div"> = {
    div: WEEKDAY_LABELS.map((label, index) => ({
      small: label,
      _key: index,
      style: { textAlign: "center" },
      $: [small({ color: "neutral" })],
    })) as unknown as DomphyElement[],
    style: calendarGridRowStyle(),
  } as unknown as DomphyElement<"div">;

  const grid: DomphyElement<"div"> = {
    div: (listener: Listener) => {
      const monthStart = startOfMonth(viewMonth.get(listener));
      const month = monthStart.getMonth();
      const gridStart = addDays(monthStart, -monthStart.getDay());
      const selected = selectedDate.get(listener);
      const weeks: DomphyElement[] = [];
      for (let week = 0; week < 6; week++) {
        const cells: DomphyElement[] = [];
        for (let day = 0; day < 7; day++) {
          const date = addDays(gridStart, week * 7 + day);
          const outside = date.getMonth() !== month;
          const isSelected = sameDay(date, selected);
          cells.push({
            button: String(date.getDate()),
            type: "button",
            disabled: outside,
            ariaSelected: isSelected,
            onClick: () => selectedDate.set(date),
            _key: isoOf(date),
            // The selected day is a fixed accent chip, not a tone-context
            // surface — same deliberate exception the core `datePicker()`
            // patch itself makes for its own selected-day cell.
            _doctorDisable: isSelected ? "tone-background-inherit" : undefined,
            style: {
              appearance: "none",
              border: "none",
              cursor: outside ? "default" : "pointer",
              aspectRatio: "1",
              borderRadius: "50%",
              opacity: outside ? 0.4 : 1,
              color: (l: Listener) => (isSelected ? themeColor(l, "shift-0", "primary") : themeColor(l, "shift-9", "neutral")),
              backgroundColor: (l: Listener) => (isSelected ? themeColor(l, "shift-9", "primary") : themeColor(l, "inherit", "neutral")),
              "&:hover:not(:disabled)": {
                backgroundColor: (l: Listener) => (isSelected ? themeColor(l, "shift-9", "primary") : themeColor(l, "shift-2", "neutral")),
              },
            },
          } as unknown as DomphyElement);
        }
        weeks.push({
          div: cells,
          _key: isoOf(addDays(gridStart, week * 7)),
          style: calendarGridRowStyle(),
        } as unknown as DomphyElement);
      }
      return weeks;
    },
    style: { display: "flex", flexDirection: "column", gap: themeSpacing(0.5) },
  } as unknown as DomphyElement<"div">;

  return {
    div: [header, weekdayHeader, grid],
    style: {
      padding: (l: Listener) => themeSpacing(themeDensity(l) * 3),
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
    },
  } as unknown as DomphyElement<"div">;
}

/** One calendar-visibility row: colored checkbox (the swatch itself, via
 * `accentColor`) + name — clicking anywhere in the row toggles visibility. */
function calendarEntryRow(entry: CalendarEntry, visibility: RecordState<Record<string, boolean>>): DomphyElement<"li"> {
  return {
    li: [
      {
        label: [
          {
            input: null,
            type: "checkbox",
            checked: (l: Listener) => visibility.get(entry.id, l),
            onChange: (e: Event) => visibility.set(entry.id, (e.target as HTMLInputElement).checked),
            $: [inputCheckbox({ color: "neutral", accentColor: entry.color })],
          } as unknown as DomphyElement,
          { span: entry.name, style: { flex: "1", textAlign: "left" } } as unknown as DomphyElement,
        ],
        style: {
          display: "flex",
          alignItems: "center",
          width: "100%",
          gap: (l: Listener) => themeSpacing(themeDensity(l) * 2),
          paddingBlock: (l: Listener) => themeSpacing(themeDensity(l) * 1.5),
          paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 1),
          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") },
        },
      } as unknown as DomphyElement,
    ],
    _key: entry.id,
  } as DomphyElement<"li">;
}

function calendarGroupSection(group: CalendarGroup, visibility: RecordState<Record<string, boolean>>): DomphyElement<"div"> {
  return {
    div: [
      {
        small: group.label,
        style: { textTransform: "uppercase", paddingInline: themeSpacing(3) },
        $: [small({ color: "neutral" })],
      } as unknown as DomphyElement,
      {
        ul: group.entries.map((entry) => calendarEntryRow(entry, visibility)),
        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) },
    _key: group.label,
  } as unknown as DomphyElement<"div">;
}

// ---------------------------------------------------------------------------
// Root assembly
// ---------------------------------------------------------------------------

/**
 * shadcn/ui "sidebar-left-right" — a Notion-like three-column workspace: a
 * primary left sidebar (org switcher, quick links, favorites, expandable
 * workspace pages, utility footer) and a secondary right sidebar (account
 * header, inline calendar, togglable calendar list) sandwiching a scrollable
 * main column. Call with no arguments for a fully working demo.
 */
function sidebarLeftRight(props: SidebarLeftRightProps = {}): DomphyElement<"div"> {
  const {
    organizations = DEFAULT_ORGANIZATIONS,
    favorites = DEFAULT_FAVORITES,
    workspaces = DEFAULT_WORKSPACES,
    footerLinks = DEFAULT_FOOTER_LINKS,
    user = DEFAULT_USER,
    calendarGroups = DEFAULT_CALENDAR_GROUPS,
    selectedDate = DEFAULT_SELECTED_DATE,
    breadcrumbLabel = "Home",
    defaultLeftCollapsed = false,
    children,
  } = props;

  const leftSidebarOpen = toState(true);
  const leftCollapsed = toState(defaultLeftCollapsed);

  const selectedDateState = toState(selectedDate);
  const viewMonthState = toState(startOfMonth(selectedDate));

  const initialVisibility: Record<string, boolean> = {};
  for (const group of calendarGroups) {
    for (const entry of group.entries) initialVisibility[entry.id] = entry.checked ?? true;
  }
  const calendarVisibility = new RecordState<Record<string, boolean>>(initialVisibility);

  const leftAside: DomphyElement<"aside"> = {
    aside: [
      renderTeamSwitcher(organizations),
      {
        ul: [
          quickLinkRow(ICON_SEARCH, "Search", leftCollapsed, { href: "#" }),
          quickLinkRow(ICON_SPARKLE, "Ask AI", leftCollapsed, { href: "#" }),
          quickLinkRow(ICON_HOME, "Home", leftCollapsed, { href: "#" }),
          quickLinkRow(ICON_INBOX, "Inbox", leftCollapsed, { href: "#", badge: 3 }),
        ],
        style: {
          listStyle: "none",
          margin: "0",
          padding: (l: Listener) => `0 ${themeSpacing(themeDensity(l) * 3)}`,
          display: "flex",
          flexDirection: "column",
          gap: themeSpacing(0.5),
          flexShrink: "0",
        },
      } as unknown as DomphyElement,
      {
        nav: [
          {
            div: [
              sectionLabel(leftCollapsed, "Favorites"),
              {
                ul: favorites.map((item) => favoriteRow(item, leftCollapsed)),
                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) },
          } as unknown as DomphyElement,
          {
            div: [
              sectionLabel(leftCollapsed, "Workspaces"),
              {
                ul: workspaces.map((group) => workspaceGroupRow(group, leftCollapsed)),
                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(4) },
          } as unknown as DomphyElement,
        ],
        style: {
          flex: "1",
          minHeight: "0",
          overflowY: "auto",
          overflowX: "hidden",
          paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          paddingBlockStart: themeSpacing(2),
        },
      } as unknown as DomphyElement,
      {
        ul: footerLinks.map((item) => footerLinkRow(item, leftCollapsed)),
        style: {
          listStyle: "none",
          margin: "0",
          padding: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          display: "flex",
          flexDirection: "column",
          gap: themeSpacing(0.5),
          flexShrink: "0",
          borderTop: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
        },
      } as unknown as DomphyElement,
      // Thin invisible edge rail — also acts as a click target to toggle collapse.
      {
        div: null,
        ariaHidden: "true",
        onClick: () => leftCollapsed.set(!leftCollapsed.get()),
        style: { position: "absolute", insetBlock: "0", insetInlineEnd: "0", width: themeSpacing(1), cursor: "col-resize" },
      } as unknown as DomphyElement,
    ],
    dataTone: "shift-2",
    _onMount: (node: ElementNode) => {
      const onKeyDown = (event: KeyboardEvent) => {
        if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "b") {
          event.preventDefault();
          leftCollapsed.set(!leftCollapsed.get());
        }
      };
      window.addEventListener("keydown", onKeyDown);
      node.addHook("Remove", () => window.removeEventListener("keydown", onKeyDown));
    },
    style: {
      position: "relative",
      display: "flex",
      flexDirection: "column",
      flexShrink: "0",
      width: (l: Listener) => (leftCollapsed.get(l) ? themeSpacing(14) : themeSpacing(64)),
      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) => (leftSidebarOpen.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: [
      {
        header: [
          {
            button: sidebarIcon(ICON_PANEL_TOGGLE),
            type: "button",
            ariaLabel: "Toggle sidebar",
            onClick: () => {
              leftSidebarOpen.set(!leftSidebarOpen.get());
              leftCollapsed.set(!leftCollapsed.get());
            },
            $: [buttonGhost({ color: "neutral" })],
          } as unknown as DomphyElement,
          verticalDivider(),
          {
            nav: [{ strong: breadcrumbLabel, ariaCurrent: "page", $: [strong({ color: "neutral" })] } as unknown as DomphyElement],
            $: [breadcrumb({ color: "neutral" })],
          } as unknown as DomphyElement,
        ],
        style: {
          position: "sticky",
          top: "0",
          zIndex: "10",
          display: "flex",
          alignItems: "center",
          gap: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          height: themeSpacing(16),
          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,
      sidebarMainContent(children),
    ],
    style: { display: "flex", flexDirection: "column", flex: "1", minWidth: "0", minHeight: "0", overflow: "auto" },
  } as unknown as DomphyElement<"main">;

  const rightAside: DomphyElement<"aside"> = {
    aside: [
      currentUserHeader(user),
      {
        div: [
          inlineMonthCalendar(viewMonthState, selectedDateState),
          {
            div: null,
            ariaHidden: "true",
            style: {
              height: "0",
              marginInline: themeSpacing(3),
              borderTop: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
            },
            _doctorDisable: "missing-color",
          } as unknown as DomphyElement,
          {
            div: calendarGroups.map((group) => calendarGroupSection(group, calendarVisibility)),
            style: { display: "flex", flexDirection: "column", gap: themeSpacing(4), padding: (l: Listener) => themeSpacing(themeDensity(l) * 3) },
          } as unknown as DomphyElement,
        ],
        style: { flex: "1", minHeight: "0", overflowY: "auto" },
      } as unknown as DomphyElement,
      {
        div: [
          {
            button: [sidebarIcon(ICON_PLUS), { span: "New Calendar", style: { flex: "1", textAlign: "left" } }],
            type: "button",
            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",
              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,
        ],
        style: {
          flexShrink: "0",
          padding: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          borderTop: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
        },
      } as unknown as DomphyElement,
    ],
    dataTone: "shift-2",
    style: {
      display: "none",
      flexDirection: "column",
      flexShrink: "0",
      width: themeSpacing(72),
      borderInlineStart: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
      backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
      // Always visible on desktop, never an animated drawer on mobile — it
      // simply appears/disappears via a plain breakpoint-driven display toggle.
      "@media (min-width: 64em)": { display: "flex" },
    },
  } as unknown as DomphyElement<"aside">;

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

export { sidebarLeftRight };
export type {
  CalendarEntry as SidebarLeftRightCalendarEntry,
  CalendarGroup as SidebarLeftRightCalendarGroup,
  CurrentUser as SidebarLeftRightUser,
  FavoriteItem as SidebarLeftRightFavoriteItem,
  FooterLink as SidebarLeftRightFooterLink,
  SidebarLeftRightProps,
  WorkspaceGroup as SidebarLeftRightWorkspaceGroup,
  WorkspacePage as SidebarLeftRightWorkspacePage,
};
export type { SidebarTeam as SidebarLeftRightOrganization } from "./sidebar05-08-shared.js";

← Back to shadcn/ui catalog