Domphy

sidebar12

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

Implementation notes

User header, a hand-built always-visible compact month-grid date picker (Sunday-first, prev/next month, adjacent-month days grayed + disabled, selected day as a filled accent circle) synced with the main header's period label/prev/next/today controls, three collapsible checkbox calendar groups (My Calendars/Favorites/Other) using inputCheckbox with a per-entry accent color, and a sticky sidebar with an independently scrolling main content area (position, not the height+overflow shell used by the rest of the family). Did not reuse @domphy/ui's datePicker() patch since that primitive is an input-triggered popover calendar, not an inline always-visible grid; built a minimal inline calendar instead. Per the spec's own research note, the main content event grid is an explicit open-ended placeholder (a plain day-number grid), not a real event/day view.

Status: ported · Reference: shadcn/ui original

// shadcn/ui "sidebar-12" — clean-room reimplementation from the public
// behavior description only (no upstream source viewed). A scheduling-app
// sidebar combining a compact month date-picker with grouped,
// checkbox-selectable calendar lists, sticky while the main content scrolls
// independently. See ./sidebar09-12-shared.ts and ./sidebar05-08-shared.ts.

import type { DomphyElement, Listener, State } from "@domphy/core";
import { toState } from "@domphy/core";
import { avatar, inputCheckbox, small, strong } from "@domphy/ui";
import { type ThemeColor, themeColor, themeDensity, themeSpacing } from "@domphy/theme";
import { ICON_PANEL_TOGGLE, ICON_PLUS, sidebarIcon, verticalDivider } from "./sidebar09-12-shared.js";

type Sidebar12User = { name: string; email: string; avatarUrl?: string };
type Sidebar12CalendarEntry = { id: string; name: string; color: ThemeColor };
type Sidebar12CalendarGroup = { label: string; entries: Sidebar12CalendarEntry[] };

type Sidebar12Props = {
  user?: Sidebar12User;
  selectedDate?: Date;
  groups?: Sidebar12CalendarGroup[];
  onDateChange?: (date: Date) => void;
  onCalendarToggle?: (groupLabel: string, entryId: string, checked: boolean) => void;
  children?: DomphyElement | DomphyElement[];
};

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

// Fixed reference date (not "today") so the zero-arg demo is deterministic —
// matches the spec's own example month/year.
const DEFAULT_DATE = new Date(2024, 9, 15);

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

const WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];

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()}`;
}

/** User avatar + name/email header, pinned above the date picker. */
function sidebarUserHeader(user: Sidebar12User): 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: [
      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,
    ],
    style: {
      display: "flex",
      alignItems: "center",
      gap: (l: Listener) => themeSpacing(themeDensity(l) * 2),
      padding: (l: Listener) => themeSpacing(themeDensity(l) * 3),
    },
  } as unknown as DomphyElement<"div">;
}

/** Compact always-visible month-grid date picker (Sunday-first, no popover). */
function monthDatePicker(viewMonth: State<Date>, selectedDate: State<Date>, onSelect: (date: Date) => void): 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: navButtonStyle(),
      } as unknown as DomphyElement,
      {
        div: (l: Listener) => monthFormatter.format(viewMonth.get(l)),
        ariaLive: "polite",
        style: { flex: "1", textAlign: "center", color: (l: Listener) => themeColor(l, "shift-9", "neutral") },
      } as unknown as DomphyElement,
      {
        button: "›",
        type: "button",
        ariaLabel: "Next month",
        onClick: () => viewMonth.set(addMonths(viewMonth.get(), 1)),
        style: navButtonStyle(),
      } 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: gridRowStyle(),
  } 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: () => onSelect(date),
            _key: isoOf(date),
            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-9", "primary") : themeColor(l, "shift-9", "neutral")),
              backgroundColor: (l: Listener) => (isSelected ? themeColor(l, "inherit", "primary") : themeColor(l, "inherit", "neutral")),
              "&:hover:not(:disabled)": {
                backgroundColor: (l: Listener) => (isSelected ? themeColor(l, "inherit", "primary") : themeColor(l, "shift-2", "neutral")),
              },
            },
          } as unknown as DomphyElement);
        }
        weeks.push({ div: cells, _key: isoOf(addDays(gridStart, week * 7)), style: gridRowStyle() } 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">;
}

function navButtonStyle() {
  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 gridRowStyle() {
  return { display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: themeSpacing(0.5) };
}

/** One calendar entry row: colored square checkbox + name. */
function calendarEntryRow(
  groupLabel: string,
  entry: Sidebar12CalendarEntry,
  checked: State<boolean>,
  onToggle?: (groupLabel: string, entryId: string, checked: boolean) => void,
): DomphyElement<"li"> {
  return {
    li: [
      {
        label: [
          {
            input: null,
            type: "checkbox",
            checked: (l: Listener) => checked.get(l),
            onChange: (e: Event) => {
              const next = (e.target as HTMLInputElement).checked;
              checked.set(next);
              onToggle?.(groupLabel, entry.id, next);
            },
            $: [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">;
}

/** A collapsible calendar group: label + chevron trigger, list of checkbox rows.
 * Matches the reference's defaults: only the first group starts expanded, and
 * only each group's first two entries start checked. */
function calendarGroupSection(
  group: Sidebar12CalendarGroup,
  groupIndex: number,
  onToggle?: (groupLabel: string, entryId: string, checked: boolean) => void,
): DomphyElement<"li"> {
  const entryStates = group.entries.map((_entry, entryIndex) => toState(entryIndex < 2));

  return {
    li: [
      {
        details: [
          {
            summary: [{ span: group.label, style: { flex: "1", textAlign: "left" } } as unknown as DomphyElement],
            style: {
              listStyle: "none",
              cursor: "pointer",
              userSelect: "none",
              display: "flex",
              alignItems: "center",
              width: "100%",
              paddingBlock: (l: Listener) => themeSpacing(themeDensity(l) * 1.5),
              paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
              color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
              backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
              "&::-webkit-details-marker": { display: "none" },
              "&::marker": { content: `""` },
            },
          } as unknown as DomphyElement,
          {
            ul: group.entries.map((entry, index) => calendarEntryRow(group.label, entry, entryStates[index]!, onToggle)),
            style: { listStyle: "none", margin: "0", padding: "0", display: "flex", flexDirection: "column", gap: themeSpacing(0.5) },
          } as unknown as DomphyElement,
        ],
        open: groupIndex === 0,
      } as unknown as DomphyElement,
    ],
    _key: group.label,
  } as DomphyElement<"li">;
}

/** Tall placeholder day-grid demonstrating the sidebar's independent scroll. */
function eventGridPlaceholder(): DomphyElement<"div"> {
  const cells = Array.from({ length: 35 }, (_unused, index) => ({
    div: String((index % 31) + 1),
    _key: index,
    dataTone: "shift-2",
    style: {
      minHeight: themeSpacing(24),
      borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 1),
      padding: themeSpacing(1),
      backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
    },
  })) as unknown as DomphyElement[];

  return {
    div: cells,
    style: {
      display: "grid",
      gridTemplateColumns: "repeat(7, minmax(0, 1fr))",
      gap: (l: Listener) => themeSpacing(themeDensity(l) * 2),
      padding: (l: Listener) => themeSpacing(themeDensity(l) * 4),
    },
  } as unknown as DomphyElement<"div">;
}

/**
 * Scheduling-app sidebar: user header, compact month date-picker, grouped
 * checkbox calendar lists — sticky while the main content scrolls
 * independently. Call with no arguments for a fully working demo.
 */
function sidebar12(props: Sidebar12Props = {}): DomphyElement<"div"> {
  const { user = DEFAULT_USER, groups = DEFAULT_GROUPS, onDateChange, onCalendarToggle, children } = props;

  const initialDate = props.selectedDate ?? DEFAULT_DATE;
  const selectedDate = toState(initialDate);
  const viewMonth = toState(startOfMonth(initialDate));
  const collapsed = toState(false);
  const mobileOpen = toState(false);

  const selectDate = (date: Date) => {
    selectedDate.set(date);
    onDateChange?.(date);
  };
  const periodLabel = (listener: Listener) =>
    new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }).format(viewMonth.get(listener));

  const asideElement: DomphyElement<"aside"> = {
    aside: [
      sidebarUserHeader(user),
      verticalDivider(),
      monthDatePicker(viewMonth, selectedDate, selectDate),
      verticalDivider(),
      {
        ul: groups.map((group, groupIndex) => calendarGroupSection(group, groupIndex, onCalendarToggle)),
        style: {
          listStyle: "none",
          margin: "0",
          padding: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          display: "flex",
          flexDirection: "column",
          gap: themeSpacing(2),
          flex: "1",
        },
      } as unknown as DomphyElement,
      {
        div: [
          {
            button: [sidebarIcon(ICON_PLUS), { span: "New Calendar", style: { flex: "1", textAlign: "left" } } as unknown as DomphyElement],
            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),
              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") },
            },
          } as unknown as DomphyElement,
        ],
        style: {
          borderTop: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
          padding: (l: Listener) => themeSpacing(themeDensity(l) * 1),
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
        },
      } as unknown as DomphyElement,
    ],
    style: {
      display: "flex",
      flexDirection: "column",
      flexShrink: "0",
      position: "sticky",
      top: "0",
      height: "100dvh",
      overflowY: "auto",
      width: (l: Listener) => (collapsed.get(l) ? "0px" : themeSpacing(70)),
      overflowX: "hidden",
      transition: "width 180ms ease-out",
      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"),
      "@media (max-width: 768px)": {
        position: "fixed",
        insetBlock: "0",
        insetInlineStart: "0",
        zIndex: "15",
        width: themeSpacing(70),
        transform: (l: Listener) => (mobileOpen.get(l) ? "translateX(0)" : "translateX(-100%)"),
        transition: "transform 180ms ease-out",
      },
    },
  } as unknown as DomphyElement<"aside">;

  const headerElement: DomphyElement<"header"> = {
    header: [
      {
        button: [sidebarIcon(ICON_PANEL_TOGGLE)],
        type: "button",
        ariaLabel: "Toggle sidebar",
        onClick: () => {
          collapsed.set(!collapsed.get());
          mobileOpen.set(!mobileOpen.get());
        },
        style: {
          appearance: "none",
          border: "none",
          background: "none",
          cursor: "pointer",
          color: (l: Listener) => themeColor(l, "shift-8", "neutral"),
        },
      } as unknown as DomphyElement,
      verticalDivider(),
      { strong: periodLabel, $: [strong({ color: "neutral" })] } as unknown as DomphyElement,
      {
        div: [
          {
            button: "Today",
            type: "button",
            onClick: () => {
              viewMonth.set(startOfMonth(initialDate));
              selectDate(initialDate);
            },
            style: todayButtonStyle(),
          } as unknown as DomphyElement,
          {
            button: "‹",
            type: "button",
            ariaLabel: "Previous period",
            onClick: () => viewMonth.set(addMonths(viewMonth.get(), -1)),
            style: navButtonStyle(),
          } as unknown as DomphyElement,
          {
            button: "›",
            type: "button",
            ariaLabel: "Next period",
            onClick: () => viewMonth.set(addMonths(viewMonth.get(), 1)),
            style: navButtonStyle(),
          } as unknown as DomphyElement,
        ],
        style: { marginInlineStart: "auto", display: "flex", alignItems: "center", gap: themeSpacing(1) },
      } as unknown as DomphyElement,
    ],
    style: {
      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<"header">;

  const mainContent: DomphyElement[] = children
    ? Array.isArray(children)
      ? children
      : [children]
    : [eventGridPlaceholder()];

  const mainElement: DomphyElement<"main"> = {
    main: [headerElement, ...mainContent],
    style: {
      display: "flex",
      flexDirection: "column",
      flex: "1",
      minWidth: "0",
      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",
      alignItems: "flex-start",
      minHeight: "100dvh",
      position: "relative",
      backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
    },
  } as unknown as DomphyElement<"div">;
}

function todayButtonStyle() {
  return {
    appearance: "none" as const,
    border: "none",
    cursor: "pointer",
    paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
    paddingBlock: (l: Listener) => themeSpacing(themeDensity(l) * 1),
    borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 1),
    color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
    backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
    outline: (l: Listener) => `1px solid ${themeColor(l, "shift-4", "neutral")}`,
    outlineOffset: "-1px",
    "&:hover": { backgroundColor: (l: Listener) => themeColor(l, "shift-2", "neutral") },
  };
}

export { sidebar12 };
export type { Sidebar12CalendarEntry, Sidebar12CalendarGroup, Sidebar12Props, Sidebar12User };

← Back to shadcn/ui catalog