From ce5146a03e6549000c5f49e7da399625e2100e53 Mon Sep 17 00:00:00 2001 From: Jesse Box Date: Mon, 6 Jul 2026 14:11:33 +0200 Subject: [PATCH 01/11] feat(breadcrumbs): Redesign with explicit typed variants and LeadingGraphics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the flat `Breadcrumbs` + `Crumb[]` API with a typed discriminated union `BreadcrumbList` + `items` array. Each breadcrumb slot now has an explicit variant ('link', 'page-title', 'select-projects') so developers must consciously choose a crumb type rather than relying on position. Key changes: - Add `BreadcrumbList` as the new preferred API with typed `BreadcrumbItem` discriminated union - Add `BreadcrumbItemLink`, `BreadcrumbItemPageTitle`, `BreadcrumbItemSelectProjects`, and `BreadcrumbItemMenuBreadcrumbs` (internal overflow) item components - Add `BreadcrumbDividerCombo` (internal) to pair items with slash dividers - Add container-query overflow: link parent items collapse into a menu below 800px container width - Add structured `pagination` prop on `BreadcrumbItemPageTitle` replacing ReactNode — callers pass {previous, next} items with `to`, `disabled`, `ariaLabel`, and optional `tooltip`; chevron buttons rendered internally - Extract `LeadingGraphics` shared component absorbing the 0/1/2+ platform icon logic from `SecondaryNavigationProjectIcon` - Refactor `SecondaryNavigationProjectIcon` to delegate to `LeadingGraphics` - Preserve the old `Breadcrumbs` + `Crumb` API as a compatibility shim so existing call sites need no changes - Add stories for `BreadcrumbList` and `LeadingGraphics` Co-Authored-By: Claude Sonnet 4.6 --- static/app/components/breadcrumbs.tsx | 138 +----------- .../breadcrumbs/breadcrumbDividerCombo.tsx | 32 +++ .../breadcrumbs/breadcrumbList.stories.tsx | 206 ++++++++++++++++++ .../components/breadcrumbs/breadcrumbList.tsx | 112 ++++++++++ static/app/components/breadcrumbs/index.tsx | 89 ++++++++ .../breadcrumbs/items/breadcrumbItemLink.tsx | 67 ++++++ .../items/breadcrumbItemMenuBreadcrumbs.tsx | 49 +++++ .../items/breadcrumbItemPageTitle.tsx | 96 ++++++++ .../items/breadcrumbItemSelectProjects.tsx | 25 +++ .../core/leadingGraphics/index.stories.tsx | 120 ++++++++++ .../components/core/leadingGraphics/index.tsx | 160 ++++++++++++++ .../views/navigation/secondary/components.tsx | 59 +---- 12 files changed, 968 insertions(+), 185 deletions(-) create mode 100644 static/app/components/breadcrumbs/breadcrumbDividerCombo.tsx create mode 100644 static/app/components/breadcrumbs/breadcrumbList.stories.tsx create mode 100644 static/app/components/breadcrumbs/breadcrumbList.tsx create mode 100644 static/app/components/breadcrumbs/index.tsx create mode 100644 static/app/components/breadcrumbs/items/breadcrumbItemLink.tsx create mode 100644 static/app/components/breadcrumbs/items/breadcrumbItemMenuBreadcrumbs.tsx create mode 100644 static/app/components/breadcrumbs/items/breadcrumbItemPageTitle.tsx create mode 100644 static/app/components/breadcrumbs/items/breadcrumbItemSelectProjects.tsx create mode 100644 static/app/components/core/leadingGraphics/index.stories.tsx create mode 100644 static/app/components/core/leadingGraphics/index.tsx diff --git a/static/app/components/breadcrumbs.tsx b/static/app/components/breadcrumbs.tsx index cdb3663ccc8b..a79510fd7983 100644 --- a/static/app/components/breadcrumbs.tsx +++ b/static/app/components/breadcrumbs.tsx @@ -1,136 +1,6 @@ -import {Fragment} from 'react'; - -import {Container, Flex} from '@sentry/scraps/layout'; -import type {LinkProps} from '@sentry/scraps/link'; -import {Link} from '@sentry/scraps/link'; -import {Text} from '@sentry/scraps/text'; - -import {extractSelectionParameters} from 'sentry/components/pageFilters/parse'; -import {IconSlashForward} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import {trackAnalytics} from 'sentry/utils/analytics'; -import {useLocation} from 'sentry/utils/useLocation'; - -export interface Crumb { - /** - * Label of the crumb - */ - label: NonNullable; - - /** - * It will keep the page filter values (projects, environments, time) in the - * querystring when navigating using extractSelectionParameters - */ - preservePageFilters?: boolean; - - /** - * Link of the crumb - */ - to?: LinkProps['to'] | null; -} - -interface BreadcrumbsProps extends React.HTMLAttributes { - crumbs: Crumb[]; - as?: 'nav'; -} - /** - * Page breadcrumbs used for navigation, not to be confused with sentry's event breadcrumbs + * Re-exports from the breadcrumbs directory. + * Import from 'sentry/components/breadcrumbs' resolves here first (file > directory), + * so existing call sites continue to work without changes. */ -export function Breadcrumbs({crumbs, as, ...props}: BreadcrumbsProps) { - if (crumbs.length === 0) { - return null; - } - - return ( - - {crumbs.map((crumb, index) => { - return ( - - - {index < crumbs.length - 1 ? ( - - - - ) : null} - - ); - })} - - ); -} - -interface BreadCrumbItemProps { - crumb: Crumb; - variant: 'primary' | 'muted'; -} - -function BreadCrumbItem(props: BreadCrumbItemProps) { - function onBreadcrumbLinkClick() { - if (props.crumb.to) { - trackAnalytics('breadcrumbs.link.clicked', {organization: null}); - } - } - - return ( - - {styleProps => { - return props.crumb.to ? ( - - - {props.crumb.label} - - - ) : ( - - {props.crumb.label} - - ); - }} - - ); -} - -interface BreadcrumbLinkProps extends LinkProps { - children?: React.ReactNode; - preservePageFilters?: boolean; -} - -function BreadcrumbLink(props: BreadcrumbLinkProps) { - const {preservePageFilters, to, ...rest} = props; - const location = useLocation(); - - if (!to) { - return ; - } - - const toWithQuery = preservePageFilters - ? typeof to === 'string' - ? {pathname: to, query: extractSelectionParameters(location.query)} - : {...to, query: {...extractSelectionParameters(location.query), ...to.query}} - : to; - - return ; -} +export * from './breadcrumbs/index'; diff --git a/static/app/components/breadcrumbs/breadcrumbDividerCombo.tsx b/static/app/components/breadcrumbs/breadcrumbDividerCombo.tsx new file mode 100644 index 000000000000..28657c656d1a --- /dev/null +++ b/static/app/components/breadcrumbs/breadcrumbDividerCombo.tsx @@ -0,0 +1,32 @@ +import type {Responsive} from '@sentry/scraps/layout'; +import {Flex} from '@sentry/scraps/layout'; + +import {IconSlashForward} from 'sentry/icons'; + +interface BreadcrumbDividerComboProps { + children: React.ReactNode; + /** Controls visibility — use responsive values for container-query toggling. */ + display?: Responsive<'flex' | 'none'>; +} + +/** + * Internal wrapper that pairs a breadcrumb item with a trailing slash divider. + * Not exported — only BreadcrumbList should use this to ensure consistent structure. + */ +export function BreadcrumbDividerCombo({children, display}: BreadcrumbDividerComboProps) { + return ( + + {children} + + + + + ); +} diff --git a/static/app/components/breadcrumbs/breadcrumbList.stories.tsx b/static/app/components/breadcrumbs/breadcrumbList.stories.tsx new file mode 100644 index 000000000000..f6b8e13aac1f --- /dev/null +++ b/static/app/components/breadcrumbs/breadcrumbList.stories.tsx @@ -0,0 +1,206 @@ +import {Fragment} from 'react'; + +import {Container} from '@sentry/scraps/layout'; +import {LeadingGraphics} from '@sentry/scraps/leadingGraphics'; + +import {BreadcrumbList} from 'sentry/components/breadcrumbs'; +import {IconSettings, IconStar} from 'sentry/icons'; +import * as Storybook from 'sentry/stories'; + +export const documentation = + import('!!type-loader!sentry/components/breadcrumbs/breadcrumbList'); + +export default Storybook.story('BreadcrumbList', story => { + story('Basic usage', () => ( + +

+ renders a typed breadcrumb trail. Pass + a discriminated {' '} + array where each entry has an explicit{' '} + 'link',{' '} + 'page-title', or 'select-projects'. The last item is + typically the current page title. +

+ + + +
+ )); + + story('With leading graphics', () => ( + +

+ Both 'link' and 'page-title' items accept an optional{' '} + rendered before + the label. Pass a node for platform + icons, sentry icons, or avatars. +

+ + } /> + ), + }, + }, + { + type: 'link', + props: { + label: 'javascript', + to: '/settings/sentry/projects/javascript/', + leadingGraphic: ( + + ), + }, + }, + { + type: 'page-title', + props: { + label: 'Client Keys (DSN)', + leadingGraphic: ( + } /> + ), + }, + }, + ]} + /> + +
+ )); + + story('Page title with pagination and trailing actions', () => ( + +

+ The 'page-title' item supports an optional structured{' '} + {' '} + slot (rendered before the label). Pass previous and next{' '} + items — each with an ariaLabel, optional to/ + onClick, disabled, and tooltip. The two + chevron buttons are rendered internally. A{' '} + slot (up to + 52px) is also available after the label. +

+ + + +
+ )); + + story('Narrow overflow (container query)', () => ( + +

+ When the container is narrower than 800px, all 'link' parent items + collapse into a single overflow button. Resize the window or + constrain the container to see this behaviour. +

+ + +

Wide (≥ 800px — all items visible)

+ + + +
+ +

Narrow ({'<'} 800px — link parents collapse into …)

+ + + +
+
+
+ )); + + story('As nav element', () => ( + +

+ Pass to render the breadcrumb + trail inside a semantic {'