Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7b7cdeb
feat(sidebar): add tokens, domain helpers, and config
joaopalopes24 Aug 30, 2026
2f3e7b2
feat(list): add iconOnly for compact navigation
joaopalopes24 Aug 30, 2026
006aa43
feat(react): add Sidebar component
joaopalopes24 Aug 30, 2026
ee7645f
feat(vue): add Sidebar component
joaopalopes24 Aug 30, 2026
a240555
docs(sidebar): document Sidebar usage
joaopalopes24 Aug 30, 2026
e5e8790
fix(sidebar): keep collapsed offcanvas rail out of the tab order
joaopalopes24 Aug 31, 2026
7d3608c
fix(list): inherit iconOnly from ancestor lists
joaopalopes24 Aug 31, 2026
7747840
feat(sidebar): add SidebarList bound to the icon rail
joaopalopes24 Aug 31, 2026
078dd5f
fix(list): compact rows and keep icons put while collapsing
joaopalopes24 Aug 31, 2026
16c351d
fix(accordion): align plain groups with sidebar nav
joaopalopes24 Aug 31, 2026
f34f325
docs(sidebar): document SidebarList and collapsible groups
joaopalopes24 Aug 31, 2026
7832505
fix(list): increase the gap between items
joaopalopes24 Aug 31, 2026
381d6c2
fix(sidebar): size the rail to its container so the footer stays visible
joaopalopes24 Aug 31, 2026
6696081
style(accordion): keep variant maps in a stable key order
joaopalopes24 Aug 31, 2026
12d201b
fix(sidebar): keep nav chrome on SidebarList
joaopalopes24 Aug 31, 2026
597ce5f
fix(sidebar): keep iconOnly on SidebarList
joaopalopes24 Aug 31, 2026
4e6201a
fix(sidebar): square collapsed header rows on the icon rail
joaopalopes24 Aug 31, 2026
d711aec
fix(sidebar): show item tooltips only on the collapsed rail
joaopalopes24 Aug 31, 2026
332a785
fix(sidebar): cap the mobile drawer to the viewport
joaopalopes24 Aug 31, 2026
0eed62e
refactor(list-item): restore listItemState and drop the unused alias
joaopalopes24 Aug 31, 2026
ac0d832
refactor(list-section): hide sections without importing Sidebar
joaopalopes24 Aug 31, 2026
4dbb7d6
fix(accordion): hide plain panels only on the collapsed rail
joaopalopes24 Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/Adapters/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const SEMANTIC_ICON_NAMES = [
"calendar",
"download",
"chevronUp",
"panelLeft",
"chevronDown",
"chevronLeft",
"chevronRight",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/Config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export type {
RadioConfigOverrides,
SelectConfigBase,
SelectConfigOverrides,
SidebarConfigBase,
SidebarConfigOverrides,
SkeletonConfigBase,
SkeletonConfigOverrides,
SliderConfigBase,
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/Config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ import type {
RadioRounded,
RadioSize,
} from "@/Tokens/Radio";
import type {
SidebarCollapsible,
SidebarCollapsibleItem,
SidebarSide,
SidebarVariant,
SidebarVariantItem,
SidebarWidth,
} from "@/Tokens/Sidebar";
import type { SkeletonRounded } from "@/Tokens/Skeleton";
import type {
SliderColor,
Expand Down Expand Up @@ -384,6 +392,7 @@ export interface PasswordFieldConfigOverrides {}
export interface ProgressConfigOverrides {}
export interface RadioConfigOverrides {}
export interface SelectConfigOverrides {}
export interface SidebarConfigOverrides {}
export interface SkeletonConfigOverrides {}
export interface SliderConfigOverrides {}
export interface SnackbarConfigOverrides {}
Expand Down Expand Up @@ -1228,6 +1237,22 @@ export interface ProgressConfigBase {
}>;
}

export interface SidebarConfigBase {
classes: object;
defaultProps: Partial<{
collapsible: keyof SidebarCollapsible;
defaultOpen: boolean;
side: keyof SidebarSide;
variant: keyof SidebarVariant;
}>;
tokens: Partial<{
collapsible: Record<string, SidebarCollapsibleItem>;
side: Record<string, string>;
variant: Record<string, SidebarVariantItem>;
width: Partial<SidebarWidth>;
}>;
}

export interface SkeletonConfigBase {
classes: object;
defaultProps: Partial<{
Expand Down Expand Up @@ -1543,6 +1568,7 @@ export type BridgeUIComponentsConfig = Partial<{
Progress: Partial<Overwrite<ProgressConfigBase, ProgressConfigOverrides>>;
Radio: Partial<Overwrite<RadioConfigBase, RadioConfigOverrides>>;
Select: Partial<Overwrite<SelectConfigBase, SelectConfigOverrides>>;
Sidebar: Partial<Overwrite<SidebarConfigBase, SidebarConfigOverrides>>;
Skeleton: Partial<Overwrite<SkeletonConfigBase, SkeletonConfigOverrides>>;
Slider: Partial<Overwrite<SliderConfigBase, SliderConfigOverrides>>;
Snackbar: Partial<Overwrite<SnackbarConfigBase, SnackbarConfigOverrides>>;
Expand Down
116 changes: 116 additions & 0 deletions packages/core/src/Domain/__tests__/sidebar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// ** External Imports
import { describe, expect, test } from "vitest";

// ** Local Imports
import {
getSidebarPanelId,
isSidebarIconOnly,
resolveSidebarCollapsibleData,
resolveSidebarListTooltipPlacement,
resolveSidebarState,
shouldRenderSidebarAsDrawer,
shouldToggleDesktopSidebar,
SIDEBAR_DESKTOP_BREAKPOINT,
toggleSidebarOpen,
} from "@/Domain/sidebar";

describe("resolveSidebarState", () => {
test("it should return expanded when collapsible is none", () => {
expect(resolveSidebarState(false, "none")).toBe("expanded");
expect(resolveSidebarState(true, "none")).toBe("expanded");
});

test("it should follow open for icon and offcanvas", () => {
expect(resolveSidebarState(true, "icon")).toBe("expanded");
expect(resolveSidebarState(false, "icon")).toBe("collapsed");
expect(resolveSidebarState(false, "offcanvas")).toBe("collapsed");
});
});

describe("shouldRenderSidebarAsDrawer", () => {
test("it should be true only on mobile", () => {
expect(shouldRenderSidebarAsDrawer(true)).toBe(true);
expect(shouldRenderSidebarAsDrawer(false)).toBe(false);
});
});

describe("toggleSidebarOpen", () => {
test("it should invert the flag", () => {
expect(toggleSidebarOpen(true)).toBe(false);
expect(toggleSidebarOpen(false)).toBe(true);
});
});

describe("resolveSidebarCollapsibleData", () => {
test("it should be empty when expanded or none", () => {
expect(resolveSidebarCollapsibleData("expanded", "icon")).toBe("");
expect(resolveSidebarCollapsibleData("collapsed", "none")).toBe("");
});

test("it should echo the mode when collapsed", () => {
expect(resolveSidebarCollapsibleData("collapsed", "icon")).toBe("icon");
expect(resolveSidebarCollapsibleData("collapsed", "offcanvas")).toBe(
"offcanvas",
);
});
});

describe("getSidebarPanelId", () => {
test("it should build a stable panel id", () => {
expect(getSidebarPanelId("sidebar-1")).toBe("sidebar-1-panel");
});
});

describe("shouldToggleDesktopSidebar", () => {
test("it should be false when collapsible is none", () => {
expect(shouldToggleDesktopSidebar("none")).toBe(false);
expect(shouldToggleDesktopSidebar("icon")).toBe(true);
expect(shouldToggleDesktopSidebar("offcanvas")).toBe(true);
});
});

describe("SIDEBAR_DESKTOP_BREAKPOINT", () => {
test("it should match the md shell breakpoint", () => {
expect(SIDEBAR_DESKTOP_BREAKPOINT).toBe("md");
});
});

describe("isSidebarIconOnly", () => {
test("it should be true only on a collapsed desktop icon rail", () => {
expect(
isSidebarIconOnly({
isMobile: false,
state: "collapsed",
collapsible: "icon",
}),
).toBe(true);
expect(
isSidebarIconOnly({
isMobile: true,
state: "collapsed",
collapsible: "icon",
}),
).toBe(false);
expect(
isSidebarIconOnly({
isMobile: false,
state: "expanded",
collapsible: "icon",
}),
).toBe(false);
expect(
isSidebarIconOnly({
isMobile: false,
state: "collapsed",
collapsible: "offcanvas",
}),
).toBe(false);
});
});

describe("resolveSidebarListTooltipPlacement", () => {
test("it should sit opposite the dock edge", () => {
expect(resolveSidebarListTooltipPlacement("left")).toBe("right");
expect(resolveSidebarListTooltipPlacement("right")).toBe("left");
});
});
15 changes: 15 additions & 0 deletions packages/core/src/Domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,21 @@ export type {
SelectOptionLike,
SelectValue,
} from "@/Domain/select";
export {
SIDEBAR_DESKTOP_BREAKPOINT,
SIDEBAR_WIDTH_ICON_VAR,
SIDEBAR_WIDTH_MOBILE_VAR,
SIDEBAR_WIDTH_VAR,
getSidebarPanelId,
isSidebarIconOnly,
resolveSidebarCollapsibleData,
resolveSidebarListTooltipPlacement,
resolveSidebarState,
shouldRenderSidebarAsDrawer,
shouldToggleDesktopSidebar,
toggleSidebarOpen,
} from "@/Domain/sidebar";
export type { SidebarCollapsibleMode, SidebarState } from "@/Domain/sidebar";
export {
DEFAULT_SLIDER_MAX,
DEFAULT_SLIDER_MIN,
Expand Down
116 changes: 116 additions & 0 deletions packages/core/src/Domain/sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Desktop visual state derived from `open` and `collapsible`.
*/
export type SidebarState = "expanded" | "collapsed";

/**
* CSS custom property for the expanded desktop rail width.
*/
export const SIDEBAR_WIDTH_VAR = "--bridge-sidebar-width";

/**
* CSS custom property for the collapsed icon-rail width.
*/
export const SIDEBAR_WIDTH_ICON_VAR = "--bridge-sidebar-width-icon";

/**
* CSS custom property for the mobile drawer panel width.
*/
export const SIDEBAR_WIDTH_MOBILE_VAR = "--bridge-sidebar-width-mobile";

/**
* Tailwind breakpoint at which the desktop rail is shown (`md:` on the shell).
* The overlay `Drawer` is used below this width, not the global `sm` mobile flag,
* so the rail is not blank between `sm` and `md`.
*/
export const SIDEBAR_DESKTOP_BREAKPOINT = "md";

/**
* Collapsible modes accepted by {@link resolveSidebarState}.
*/
export type SidebarCollapsibleMode = "icon" | "none" | "offcanvas";

/**
* Resolves `expanded` vs `collapsed` for desktop chrome.
* `none` is always expanded. Mobile overlay is independent (`openMobile`).
*/
export function resolveSidebarState(
open: boolean,
collapsible: SidebarCollapsibleMode,
): SidebarState {
if (collapsible === "none") {
return "expanded";
}

return open ? "expanded" : "collapsed";
}

/**
* Whether the sidebar should mount its mobile `Drawer` overlay.
* Desktop chrome stays mounted and is hidden with CSS (`md:`).
*/
export function shouldRenderSidebarAsDrawer(isMobile: boolean): boolean {
return isMobile;
}

/**
* Next boolean for a sidebar open flag (desktop `open` or mobile `openMobile`).
*/
export function toggleSidebarOpen(open: boolean): boolean {
return !open;
}

/**
* `data-collapsible` value: the mode when collapsed, empty when expanded.
*/
export function resolveSidebarCollapsibleData(
state: SidebarState,
collapsible: SidebarCollapsibleMode,
): "" | SidebarCollapsibleMode {
if (state === "expanded" || collapsible === "none") {
return "";
}

return collapsible;
}

/**
* Stable DOM id for the sidebar panel (`aria-controls` on the trigger).
*/
export function getSidebarPanelId(sidebarId: string): string {
return `${sidebarId}-panel`;
}

/**
* Whether desktop toggle should change `open`. `none` is a no-op on desktop.
*/
export function shouldToggleDesktopSidebar(
collapsible: SidebarCollapsibleMode,
): boolean {
return collapsible !== "none";
}

/**
* Whether rail lists should collapse to icons.
* False below the desktop breakpoint so the overlay drawer keeps labels.
*/
export function isSidebarIconOnly({
state,
isMobile,
collapsible,
}: {
collapsible: SidebarCollapsibleMode;
isMobile: boolean;
state: SidebarState;
}): boolean {
return !isMobile && collapsible === "icon" && state === "collapsed";
}

/**
* Tooltip placement for collapsed icon-rail items. Opposite the dock edge.
*/
export function resolveSidebarListTooltipPlacement(
side: "left" | "right",
): "left" | "right" {
return side === "right" ? "left" : "right";
}
22 changes: 16 additions & 6 deletions packages/core/src/Tokens/Accordion/Variant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export interface AccordionVariantItem {
*/
"item": string;

/**
* Classes for the expandable panel region.
*/
"panel": string;

/**
* Classes for the accordion root.
*/
Expand Down Expand Up @@ -50,20 +55,16 @@ export interface AccordionVariant {
* Default accordion variant class maps.
*/
export const variantProps: AccordionVariant = {
"plain": {
"root": "flex flex-col gap-1",
"item": "overflow-hidden rounded-lg",
"trigger":
"text-dark-700 hover:bg-dark-500/5 dark:text-dark-200 dark:hover:bg-dark-500/10",
},
"default": {
"item": "",
"panel": "",
"trigger":
"text-dark-700 hover:bg-dark-500/5 dark:text-dark-200 dark:hover:bg-dark-500/10",
"root":
"divide-y divide-dark-200 border-y border-dark-200 dark:divide-dark-700 dark:border-dark-700",
},
"separated": {
"panel": "",
"root": "flex flex-col gap-2",
"item":
"overflow-hidden rounded-lg border border-dark-200 dark:border-dark-700",
Expand All @@ -72,9 +73,18 @@ export const variantProps: AccordionVariant = {
},
"outlined": {
"item": "",
"panel": "",
"trigger":
"text-dark-700 hover:bg-dark-500/5 dark:text-dark-200 dark:hover:bg-dark-500/10",
"root":
"overflow-hidden rounded-lg border border-dark-200 divide-y divide-dark-200 dark:border-dark-700 dark:divide-dark-700",
},
"plain": {
"item": "",
"root": "flex flex-col gap-1 px-2 py-2",
"trigger":
"rounded-lg min-h-8 px-2 py-1.5 text-dark-700 hover:bg-black/5 dark:text-dark-200 dark:hover:bg-white/10",
"panel":
"ml-3.5 translate-x-px border-l border-dark-200 p-0 py-0.5 pl-2.5 group-data-[collapsible=icon]:hidden dark:border-dark-700",
},
};
Loading
Loading