From 86040e7bb2dee7829eebaac8cadb7b6dfe4e8dec Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Tue, 23 Jun 2026 16:10:54 +0700 Subject: [PATCH 1/4] Align breadcrumb to architecture goals Move all className composition into cva in variants.ts (ui and components tiers), remove every cx() call from component files, add new variants for BreadcrumbItem / BreadcrumbSeparator / the ordered list, merge the two separate trigger cva calls into one, move BreadcrumbPage's text-primary into the interactive:false variant, move BreadcrumbDropdownItem's className into a cva, and replace the icon wrapper in BreadcrumbMenuTrigger with a named slot variant. Separator now sizes its default chevron via --node-size / [&>svg] instead of a hardcoded className on the child, and RTL mirroring is baked into the separator variant. Closes #121. --- .../breadcrumb/breadcrumb-dropdown-item.tsx | 9 +++---- .../breadcrumb/breadcrumb-dropdown.tsx | 8 ++++-- .../breadcrumb/breadcrumb-menu-trigger.tsx | 8 +++--- .../breadcrumb/breadcrumb.stories.tsx | 8 ++---- .../src/components/breadcrumb/variants.ts | 26 +++++++++++++++++++ .../src/ui/breadcrumb/breadcrumb-item.tsx | 4 ++- .../src/ui/breadcrumb/breadcrumb-link.tsx | 3 +-- .../src/ui/breadcrumb/breadcrumb-page.tsx | 9 +------ .../ui/breadcrumb/breadcrumb-separator.tsx | 16 ++++++------ .../src/ui/breadcrumb/breadcrumb-trigger.tsx | 5 ++-- .../propel/src/ui/breadcrumb/breadcrumb.tsx | 4 ++- packages/propel/src/ui/breadcrumb/index.tsx | 8 +++++- packages/propel/src/ui/breadcrumb/variants.ts | 26 +++++++++++++++++-- 13 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 packages/propel/src/components/breadcrumb/variants.ts diff --git a/packages/propel/src/components/breadcrumb/breadcrumb-dropdown-item.tsx b/packages/propel/src/components/breadcrumb/breadcrumb-dropdown-item.tsx index 7e87ef8b..7d6b8be9 100644 --- a/packages/propel/src/components/breadcrumb/breadcrumb-dropdown-item.tsx +++ b/packages/propel/src/components/breadcrumb/breadcrumb-dropdown-item.tsx @@ -1,13 +1,10 @@ import { Menu } from "@base-ui/react/menu"; +import { crumbDropdownItemVariants } from "./variants"; + export type BreadcrumbDropdownItemProps = Omit; /** A single item inside `BreadcrumbDropdown`. */ export function BreadcrumbDropdownItem(props: BreadcrumbDropdownItemProps) { - return ( - - ); + return ; } diff --git a/packages/propel/src/components/breadcrumb/breadcrumb-dropdown.tsx b/packages/propel/src/components/breadcrumb/breadcrumb-dropdown.tsx index e616ebe3..33c99da6 100644 --- a/packages/propel/src/components/breadcrumb/breadcrumb-dropdown.tsx +++ b/packages/propel/src/components/breadcrumb/breadcrumb-dropdown.tsx @@ -2,8 +2,10 @@ import { Menu } from "@base-ui/react/menu"; import { Ellipsis } from "lucide-react"; import type * as React from "react"; +import { NodeSlot } from "../../internal/node-slot"; import { OverlayPanel } from "../../internal/overlay-panel"; import { BreadcrumbTrigger } from "../../ui/breadcrumb"; +import { crumbDropdownPopupVariants } from "./variants"; export type BreadcrumbDropdownProps = Omit & { /** The collapsed crumbs shown in the menu. */ @@ -21,12 +23,14 @@ export function BreadcrumbDropdown({ return ( } {...props}> - + + + - {children} + {children} diff --git a/packages/propel/src/components/breadcrumb/breadcrumb-menu-trigger.tsx b/packages/propel/src/components/breadcrumb/breadcrumb-menu-trigger.tsx index 32d657ba..7758f1ca 100644 --- a/packages/propel/src/components/breadcrumb/breadcrumb-menu-trigger.tsx +++ b/packages/propel/src/components/breadcrumb/breadcrumb-menu-trigger.tsx @@ -3,6 +3,7 @@ import { ChevronRight } from "lucide-react"; import type * as React from "react"; import { BreadcrumbTrigger } from "../../ui/breadcrumb"; +import { crumbMenuTriggerIconSlotVariants, crumbMenuTriggerIndicatorVariants } from "./variants"; export type BreadcrumbMenuTriggerProps = Omit & { /** Leading content, typically a work-item/page icon. */ @@ -16,15 +17,12 @@ export function BreadcrumbMenuTrigger({ icon, children, ...props }: BreadcrumbMe return ( } {...props}> {icon != null ? ( - + {icon} ) : null} {children} - ); } diff --git a/packages/propel/src/components/breadcrumb/breadcrumb.stories.tsx b/packages/propel/src/components/breadcrumb/breadcrumb.stories.tsx index b816ed7e..3c7dba22 100644 --- a/packages/propel/src/components/breadcrumb/breadcrumb.stories.tsx +++ b/packages/propel/src/components/breadcrumb/breadcrumb.stories.tsx @@ -177,9 +177,7 @@ export const WithMenuCrumb: Story = { - }> - Plane Design - + }>Plane Design @@ -249,9 +247,7 @@ export const KeyboardNavigation: Story = { - }> - Plane Design - + }>Plane Design diff --git a/packages/propel/src/components/breadcrumb/variants.ts b/packages/propel/src/components/breadcrumb/variants.ts new file mode 100644 index 00000000..e9e5ea56 --- /dev/null +++ b/packages/propel/src/components/breadcrumb/variants.ts @@ -0,0 +1,26 @@ +import { cva } from "class-variance-authority"; + +/** A single row inside `BreadcrumbDropdown`'s menu. */ +export const crumbDropdownItemVariants = cva( + "flex cursor-default items-center rounded-sm px-2 py-1 text-14 leading-[1.54] text-secondary outline-none select-none data-highlighted:bg-layer-transparent-hover data-highlighted:text-primary", +); + +/** The `` popup inside `BreadcrumbDropdown`. */ +export const crumbDropdownPopupVariants = cva("p-1 outline-none"); + +/** + * Icon slot for `BreadcrumbMenuTrigger`'s optional leading icon. Sizes the child to 16 px via + * `--node-size`; wraps with the standard NodeSlot layout utilities so no className is needed on the + * icon element itself. + */ +export const crumbMenuTriggerIconSlotVariants = cva( + "inline-flex shrink-0 items-center justify-center text-icon-tertiary [--node-size:1rem] [&>img]:size-(--node-size) [&>svg]:size-(--node-size)", +); + +/** + * The trailing chevron inside `BreadcrumbMenuTrigger` — fixed 14 px, rotates 90° when the menu is + * open, and mirrors in RTL. + */ +export const crumbMenuTriggerIndicatorVariants = cva( + "size-3.5 shrink-0 text-icon-tertiary transition-transform group-data-popup-open/trigger:rotate-90 rtl:not-group-data-popup-open/trigger:-scale-x-100", +); diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-item.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-item.tsx index 3a43fac9..59246471 100644 --- a/packages/propel/src/ui/breadcrumb/breadcrumb-item.tsx +++ b/packages/propel/src/ui/breadcrumb/breadcrumb-item.tsx @@ -1,8 +1,10 @@ import type * as React from "react"; +import { crumbItemVariants } from "./variants"; + export type BreadcrumbItemProps = Omit, "className" | "style">; /** One step in the trail: a list item holding a link, page, or dropdown crumb. */ export function BreadcrumbItem(props: BreadcrumbItemProps) { - return
  • ; + return
  • ; } diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-link.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-link.tsx index 92368edd..23c1dee3 100644 --- a/packages/propel/src/ui/breadcrumb/breadcrumb-link.tsx +++ b/packages/propel/src/ui/breadcrumb/breadcrumb-link.tsx @@ -1,4 +1,3 @@ -import { cx } from "class-variance-authority"; import type * as React from "react"; import { crumbVariants } from "./variants"; @@ -7,5 +6,5 @@ export type BreadcrumbLinkProps = Omit, "className" | /** A navigable crumb — renders an anchor styled as a hoverable pill. */ export function BreadcrumbLink(props: BreadcrumbLinkProps) { - return ; + return ; } diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-page.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-page.tsx index 0f0574c7..4b13502e 100644 --- a/packages/propel/src/ui/breadcrumb/breadcrumb-page.tsx +++ b/packages/propel/src/ui/breadcrumb/breadcrumb-page.tsx @@ -1,4 +1,3 @@ -import { cx } from "class-variance-authority"; import type * as React from "react"; import { crumbVariants } from "./variants"; @@ -7,11 +6,5 @@ export type BreadcrumbPageProps = Omit, "className" /** The current page — the last, non-navigable crumb. */ export function BreadcrumbPage(props: BreadcrumbPageProps) { - return ( - - ); + return ; } diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-separator.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-separator.tsx index 761ddd44..39b8e498 100644 --- a/packages/propel/src/ui/breadcrumb/breadcrumb-separator.tsx +++ b/packages/propel/src/ui/breadcrumb/breadcrumb-separator.tsx @@ -1,18 +1,18 @@ import { ChevronRight } from "lucide-react"; import type * as React from "react"; +import { crumbSeparatorVariants } from "./variants"; + export type BreadcrumbSeparatorProps = Omit, "className" | "style">; -/** The visual divider between crumbs. */ +/** + * The visual divider between crumbs. Pass a custom icon or character as `children`; defaults to a + * chevron. + */ export function BreadcrumbSeparator({ children, ...props }: BreadcrumbSeparatorProps) { return ( -
  • - {children ?? } +
  • + {children ?? }
  • ); } diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb-trigger.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb-trigger.tsx index 775c7c56..fdd6ed5c 100644 --- a/packages/propel/src/ui/breadcrumb/breadcrumb-trigger.tsx +++ b/packages/propel/src/ui/breadcrumb/breadcrumb-trigger.tsx @@ -1,8 +1,7 @@ import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; -import { cx } from "class-variance-authority"; -import { crumbTriggerVariants, crumbVariants } from "./variants"; +import { crumbTriggerVariants } from "./variants"; export type BreadcrumbTriggerProps = Omit< useRender.ComponentProps<"button">, @@ -19,7 +18,7 @@ export type BreadcrumbTriggerProps = Omit< export function BreadcrumbTrigger({ group = false, render, ...props }: BreadcrumbTriggerProps) { const defaultProps: useRender.ElementProps<"button"> = { ...(render == null ? { type: "button" } : null), - className: cx(crumbVariants({ interactive: true }), crumbTriggerVariants({ group })), + className: crumbTriggerVariants({ group }), }; return useRender({ diff --git a/packages/propel/src/ui/breadcrumb/breadcrumb.tsx b/packages/propel/src/ui/breadcrumb/breadcrumb.tsx index 66ae930a..0d20fe44 100644 --- a/packages/propel/src/ui/breadcrumb/breadcrumb.tsx +++ b/packages/propel/src/ui/breadcrumb/breadcrumb.tsx @@ -1,12 +1,14 @@ import type * as React from "react"; +import { crumbListVariants } from "./variants"; + export type BreadcrumbProps = Omit, "className" | "style">; /** Breadcrumb trail: a `