diff --git a/.changeset/timeline-badge-variant-clip-sidebar.md b/.changeset/timeline-badge-variant-clip-sidebar.md new file mode 100644 index 00000000000..15f8f2387c5 --- /dev/null +++ b/.changeset/timeline-badge-variant-clip-sidebar.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Timeline: add `variant` prop to `Timeline.Badge` for built-in color schemes, and extend `clipSidebar` to accept `'start'` or `'end'` for one-sided trimming diff --git a/e2e/components/Timeline.test.ts b/e2e/components/Timeline.test.ts index ed8a8d4953a..53ed90c5677 100644 --- a/e2e/components/Timeline.test.ts +++ b/e2e/components/Timeline.test.ts @@ -11,6 +11,14 @@ const stories = [ title: 'Clip Sidebar', id: 'components-timeline-features--clip-sidebar', }, + { + title: 'Clip Sidebar Start', + id: 'components-timeline-features--clip-sidebar-start', + }, + { + title: 'Clip Sidebar End', + id: 'components-timeline-features--clip-sidebar-end', + }, { title: 'Condensed Items', id: 'components-timeline-features--condensed-items', @@ -19,6 +27,10 @@ const stories = [ title: 'Timeline Break', id: 'components-timeline-features--timeline-break', }, + { + title: 'Badge Variants', + id: 'components-timeline-features--badge-variants', + }, ] as const test.describe('Timeline', () => { diff --git a/packages/react/src/Timeline/Timeline.docs.json b/packages/react/src/Timeline/Timeline.docs.json index 35f24010491..a8bb8c2d135 100644 --- a/packages/react/src/Timeline/Timeline.docs.json +++ b/packages/react/src/Timeline/Timeline.docs.json @@ -10,12 +10,21 @@ { "id": "components-timeline-features--clip-sidebar" }, + { + "id": "components-timeline-features--clip-sidebar-start" + }, + { + "id": "components-timeline-features--clip-sidebar-end" + }, { "id": "components-timeline-features--condensed-items" }, { "id": "components-timeline-features--timeline-break" }, + { + "id": "components-timeline-features--badge-variants" + }, { "id": "components-timeline-features--with-inline-links" } @@ -24,8 +33,8 @@ "props": [ { "name": "clipSidebar", - "type": "boolean", - "description": "Hides the sidebar above the first Timeline.Item and below the last Timeline.Item." + "type": "boolean | 'start' | 'end'", + "description": "Clips the sidebar line. When true, clips both ends. Use 'start' or 'end' to clip only one end." } ], "subcomponents": [ @@ -41,7 +50,13 @@ }, { "name": "Timeline.Badge", - "props": [] + "props": [ + { + "name": "variant", + "type": "'accent' | 'success' | 'attention' | 'severe' | 'danger' | 'done' | 'open' | 'closed' | 'sponsors'", + "description": "The color variant of the badge." + } + ] }, { "name": "Timeline.Body", diff --git a/packages/react/src/Timeline/Timeline.features.stories.module.css b/packages/react/src/Timeline/Timeline.features.stories.module.css index 8b13298a377..f7da0223915 100644 --- a/packages/react/src/Timeline/Timeline.features.stories.module.css +++ b/packages/react/src/Timeline/Timeline.features.stories.module.css @@ -1,7 +1,3 @@ -.BadgeWithDoneBackground { - background-color: var(--bgColor-done-emphasis); -} - .LinkWithBoldStyle { font-weight: var(--base-text-weight-semibold); color: var(--fgColor-default); @@ -11,7 +7,3 @@ .LinkWithBoldStyle:hover { color: var(--fgColor-accent); } - -.GitMergeIcon { - color: var(--fgColor-onEmphasis); -} diff --git a/packages/react/src/Timeline/Timeline.features.stories.tsx b/packages/react/src/Timeline/Timeline.features.stories.tsx index c924b48ee7f..34875f3a739 100644 --- a/packages/react/src/Timeline/Timeline.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.features.stories.tsx @@ -2,7 +2,18 @@ import type {Meta} from '@storybook/react-vite' import type {ComponentProps} from '../utils/types' import Timeline from './Timeline' import Octicon from '../Octicon' -import {GitBranchIcon, GitCommitIcon, GitMergeIcon} from '@primer/octicons-react' +import { + FlameIcon, + GitBranchIcon, + GitCommitIcon, + GitMergeIcon, + GitPullRequestIcon, + HeartIcon, + IssueClosedIcon, + IssueOpenedIcon, + SkipIcon, + XIcon, +} from '@primer/octicons-react' import Link from '../Link' import classes from './Timeline.features.stories.module.css' @@ -34,6 +45,40 @@ export const ClipSidebar = () => ( ) +export const ClipSidebarStart = () => ( + + + + + + This is a message + + + + + + This is a message + + +) + +export const ClipSidebarEnd = () => ( + + + + + + This is a message + + + + + + This is a message + + +) + export const CondensedItems = () => ( @@ -67,8 +112,8 @@ export const CondensedItems = () => ( export const TimelineBreak = () => ( - - + + This is a message @@ -82,6 +127,65 @@ export const TimelineBreak = () => ( ) +export const BadgeVariants = () => ( + + + + + + Accent + + + + + + Success + + + + + + Attention + + + + + + Severe + + + + + + Danger + + + + + + Done + + + + + + Open + + + + + + Closed + + + + + + Sponsors + + +) + export const WithInlineLinks = () => ( diff --git a/packages/react/src/Timeline/Timeline.module.css b/packages/react/src/Timeline/Timeline.module.css index f90a195d71b..3458e8e3907 100644 --- a/packages/react/src/Timeline/Timeline.module.css +++ b/packages/react/src/Timeline/Timeline.module.css @@ -2,7 +2,7 @@ display: flex; flex-direction: column; - &:where([data-clip-sidebar]) { + &:where([data-clip-sidebar=''], [data-clip-sidebar='start']) { .TimelineItem:first-child { padding-top: 0; @@ -10,7 +10,9 @@ top: var(--base-size-12); } } + } + &:where([data-clip-sidebar=''], [data-clip-sidebar='end']) { .TimelineItem:last-child { padding-bottom: 0; @@ -83,6 +85,47 @@ border-radius: 50%; align-items: center; justify-content: center; + + &:where([data-variant]) { + color: var(--fgColor-onEmphasis); + border-color: transparent; + } + + &:where([data-variant='accent']) { + background-color: var(--bgColor-accent-emphasis); + } + + &:where([data-variant='success']) { + background-color: var(--bgColor-success-emphasis); + } + + &:where([data-variant='attention']) { + background-color: var(--bgColor-attention-emphasis); + } + + &:where([data-variant='severe']) { + background-color: var(--bgColor-severe-emphasis); + } + + &:where([data-variant='danger']) { + background-color: var(--bgColor-danger-emphasis); + } + + &:where([data-variant='done']) { + background-color: var(--bgColor-done-emphasis); + } + + &:where([data-variant='open']) { + background-color: var(--bgColor-open-emphasis); + } + + &:where([data-variant='closed']) { + background-color: var(--bgColor-closed-emphasis); + } + + &:where([data-variant='sponsors']) { + background-color: var(--bgColor-sponsors-emphasis); + } } .TimelineBody { diff --git a/packages/react/src/Timeline/Timeline.tsx b/packages/react/src/Timeline/Timeline.tsx index 772acbbfff8..db3bcf05590 100644 --- a/packages/react/src/Timeline/Timeline.tsx +++ b/packages/react/src/Timeline/Timeline.tsx @@ -2,17 +2,23 @@ import {clsx} from 'clsx' import React from 'react' import classes from './Timeline.module.css' -type StyledTimelineProps = {clipSidebar?: boolean; className?: string} +type StyledTimelineProps = {clipSidebar?: boolean | 'start' | 'end'; className?: string} export type TimelineProps = StyledTimelineProps & React.ComponentPropsWithoutRef<'div'> +function clipSidebarValue(clipSidebar: TimelineProps['clipSidebar']): string | undefined { + if (clipSidebar === true) return '' + if (clipSidebar === 'start' || clipSidebar === 'end') return clipSidebar + return undefined +} + const Timeline = React.forwardRef(({clipSidebar, className, ...props}, forwardRef) => { return (
) }) @@ -43,15 +49,28 @@ const TimelineItem = React.forwardRef( TimelineItem.displayName = 'TimelineItem' +export type TimelineBadgeVariant = + | 'accent' + | 'success' + | 'attention' + | 'severe' + | 'danger' + | 'done' + | 'open' + | 'closed' + | 'sponsors' + export type TimelineBadgeProps = { children?: React.ReactNode className?: string + /** The color variant of the badge */ + variant?: TimelineBadgeVariant } & React.ComponentPropsWithoutRef<'div'> -const TimelineBadge = ({className, ...props}: TimelineBadgeProps) => { +const TimelineBadge = ({className, variant, ...props}: TimelineBadgeProps) => { return (
-
+
) } diff --git a/packages/react/src/Timeline/__tests__/Timeline.test.tsx b/packages/react/src/Timeline/__tests__/Timeline.test.tsx index 30715ddbef7..453cb08e378 100644 --- a/packages/react/src/Timeline/__tests__/Timeline.test.tsx +++ b/packages/react/src/Timeline/__tests__/Timeline.test.tsx @@ -11,6 +11,16 @@ describe('Timeline', () => { const {container} = render() expect(container).toMatchSnapshot() }) + + it('renders with clipSidebar="start"', () => { + const {container} = render() + expect(container.firstChild).toHaveAttribute('data-clip-sidebar', 'start') + }) + + it('renders with clipSidebar="end"', () => { + const {container} = render() + expect(container.firstChild).toHaveAttribute('data-clip-sidebar', 'end') + }) }) describe('Timeline.Item', () => { @@ -28,6 +38,16 @@ describe('Timeline.Item', () => { describe('Timeline.Badge', () => { implementsClassName(Timeline.Badge, classes.TimelineBadge) + + it('renders with variant prop', () => { + const {container} = render() + expect(container.querySelector(`.${classes.TimelineBadge}`)).toHaveAttribute('data-variant', 'done') + }) + + it('does not render data-variant when variant is omitted', () => { + const {container} = render() + expect(container.querySelector(`.${classes.TimelineBadge}`)).not.toHaveAttribute('data-variant') + }) }) describe('Timeline.Body', () => { diff --git a/packages/react/src/Timeline/index.ts b/packages/react/src/Timeline/index.ts index 55b0dce49f6..e1a60723c81 100644 --- a/packages/react/src/Timeline/index.ts +++ b/packages/react/src/Timeline/index.ts @@ -3,6 +3,7 @@ export type { TimelineProps, TimelineItemsProps, TimelineItemProps, + TimelineBadgeVariant, TimelineBadgeProps, TimelineBodyProps, TimelineBreakProps, diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 81c0fae44ad..d3cc41e64d9 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -177,6 +177,7 @@ export type {TextProps} from './Text' export {default as Timeline} from './Timeline' export type { TimelineProps, + TimelineBadgeVariant, TimelineBadgeProps, TimelineBodyProps, TimelineBreakProps,