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.
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.
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 { toState } from "@domphy/core";
import { avatar, buttonGhost, icon, popover, small } from "@domphy/ui";
import { themeColor, themeDensity, themeSpacing } from "@domphy/theme";
import {
ICON_CHEVRON_RIGHT,
ICON_FOLDER,
ICON_GRID,
ICON_INBOX,
ICON_LIFEBUOY,
ICON_MORE,
ICON_PANEL_TOGGLE,
ICON_PLUS,
ICON_SEARCH,
ICON_TRASH,
emojiGlyph,
interactiveRowStyle,
renderPlainNavRow,
renderTeamSwitcher,
renderUserFooter,
sidebarBreadcrumb,
sidebarIcon,
sidebarMainContent,
sidebarStyledPopoverContent,
useShowMore,
verticalDivider,
type SidebarBreadcrumbItem,
type SidebarNavMainItem,
type SidebarTeam,
type SidebarUser,
} from "./sidebar09-12-shared.js";
import { ICON_CALENDAR, ICON_HOME, ICON_SETTINGS, ICON_SPARKLE } from "./sidebar09-12-shared.js";
type Sidebar10FavoriteItem = { emoji: string; label: string; href?: string };
type Sidebar10Page = { title: string; href?: string };
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[];
user?: SidebarUser;
breadcrumbItems?: SidebarBreadcrumbItem[];
children?: DomphyElement | DomphyElement[];
};
const DEFAULT_TEAMS: SidebarTeam[] = [
{ name: "Acme Inc", plan: "Enterprise" },
{ name: "Acme Corp", plan: "Startup" },
];
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: "🛠️",
expanded: true,
pages: [{ title: "Architecture" }, { title: "RFCs" }, { title: "On-call" }],
},
{ name: "Product", emoji: "📦", pages: [{ title: "Roadmap" }, { title: "Feedback" }, { title: "Specs" }] },
{ name: "Design", emoji: "🎨", pages: [{ title: "Components" }, { title: "Tokens" }] },
{ name: "Marketing", emoji: "📣", pages: [{ title: "Campaigns" }, { title: "Brand" }, { title: "Content" }] },
{ name: "Sales", emoji: "💼", pages: [{ title: "Pipeline" }, { title: "Playbook" }] },
{ name: "Finance", emoji: "💰", pages: [{ title: "Forecasts" }, { 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 },
];
const DEFAULT_USER: SidebarUser = { name: "Shad Cn", email: "shadcn@example.com" };
/** 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: [{ label: "Rename" }, { label: "Copy link" }, { label: "Remove from favorites" }] },
]);
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: `<details>` accordion + hover add-page button + nested page list. */
function workspaceNode(workspace: Sidebar10Workspace, collapsed: State<boolean>): DomphyElement<"li"> {
return {
li: [
{
details: [
{
summary: [
emojiGlyph(workspace.emoji),
{ span: workspace.name, style: { flex: "1", textAlign: "left" } } 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,
{
span: ICON_CHEVRON_RIGHT,
dataSlot: "chevron",
style: { transition: "transform 150ms ease" },
$: [icon({ color: "neutral" })],
} as unknown as DomphyElement,
],
style: {
listStyle: "none",
cursor: "pointer",
userSelect: "none",
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"),
"&::-webkit-details-marker": { display: "none" },
"&::marker": { content: `""` },
"&:hover": { backgroundColor: (l: Listener) => themeColor(l, "shift-2", "neutral") },
"&:hover [data-slot=row-add]": { display: "inline-flex" },
},
} as unknown as DomphyElement,
{
ul: workspace.pages.map((page, index) => ({
li: [
{
a: [{ span: page.title, style: { flex: "1", textAlign: "left" } } as unknown as DomphyElement],
href: page.href ?? "#",
style: {
display: "flex",
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),
paddingBlock: "0",
paddingInlineEnd: "0",
borderInlineStart: (l: Listener) => `1px solid ${themeColor(l, "shift-3", "neutral")}`,
color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
maxHeight: "0px",
overflow: "hidden",
opacity: "0",
transition: "max-height 180ms linear, opacity 180ms linear",
},
} as unknown as DomphyElement,
],
open: workspace.expanded ?? false,
style: {
display: (l: Listener) => (collapsed.get(l) ? "none" : "block"),
"&[open] summary [data-slot=chevron]": { transform: "rotate(90deg)" },
"&[open] > ul": { maxHeight: themeSpacing(240), opacity: "1", paddingBlock: themeSpacing(1) },
},
} as unknown as DomphyElement,
],
_key: workspace.name,
} 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">;
}
/** Overlapping avatar-stack shown in the main header's nav-actions cluster. */
function avatarStack(names: string[]): DomphyElement<"div"> {
return {
div: names.map((name, index) => ({
span: name.slice(0, 1).toUpperCase(),
_key: index,
style: {
marginInlineStart: index === 0 ? "0" : `-${themeSpacing(2)}`,
outline: (l: Listener) => `2px solid ${themeColor(l, "inherit", "neutral")}`,
outlineOffset: "0",
color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
},
$: [avatar({ color: index % 2 === 0 ? "primary" : "neutral" })],
})) as unknown as DomphyElement[],
style: { display: "flex", alignItems: "center" },
} as unknown as DomphyElement<"div">;
}
function mainHeader(props: {
onToggle: () => void;
breadcrumbItems: SidebarBreadcrumbItem[];
memberNames: string[];
}): DomphyElement<"header"> {
const actionsMenu = sidebarStyledPopoverContent([
{ items: [{ icon: ICON_SEARCH, label: "Search" }, { icon: ICON_GRID, label: "Templates" }] },
{ items: [{ icon: ICON_TRASH, label: "Trash" }, { label: "Invite members" }] },
]);
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: [
avatarStack(props.memberNames),
{
button: sidebarIcon(ICON_MORE),
type: "button",
ariaLabel: "More actions",
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(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">;
}
/**
* 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,
user = DEFAULT_USER,
breadcrumbItems = [{ label: "Engineering" }, { label: "Architecture" }],
children,
} = props;
const collapsed = toState(false);
const favoritesShowMore = useShowMore(favorites, favoritesVisibleCount);
const workspacesShowMore = useShowMore(workspaces, workspacesVisibleCount);
const quickLinks: SidebarNavMainItem[] = [
{ title: "Search", icon: ICON_SEARCH, href: "#" },
{ title: "Ask AI", icon: ICON_SPARKLE, href: "#" },
{ title: "Home", icon: ICON_HOME, href: "#" },
{ title: "Inbox", icon: ICON_INBOX, href: "#" },
];
const asideElement: DomphyElement<"aside"> = {
aside: [
renderTeamSwitcher(teams),
{
nav: [
{
ul: quickLinks.map((item) => renderPlainNavRow(item, collapsed)),
style: { listStyle: "none", margin: "0", padding: "0", display: "flex", flexDirection: "column", gap: themeSpacing(0.5) },
} as unknown as DomphyElement,
{
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),
},
} 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,
renderUserFooter(user),
],
style: {
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,
memberNames: [user.name, "Alex Rivera", "Jordan Lee"],
}),
sidebarMainContent(children),
],
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,
};