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,