diff --git a/.changeset/timeline-actions-slot.md b/.changeset/timeline-actions-slot.md new file mode 100644 index 00000000000..18de76eadea --- /dev/null +++ b/.changeset/timeline-actions-slot.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Add `Timeline.Actions` sub-component for rendering action buttons, links, SHAs, status labels, and similar right-aligned content on a `Timeline.Item`. Renders as a horizontal flex row pushed to the right edge of the item with `gap: 8px` between children and `min-height` matching the badge so contents vertically center against it. diff --git a/packages/react/src/Timeline/Timeline.docs.json b/packages/react/src/Timeline/Timeline.docs.json index b3a1895e0a6..8694716be77 100644 --- a/packages/react/src/Timeline/Timeline.docs.json +++ b/packages/react/src/Timeline/Timeline.docs.json @@ -27,6 +27,9 @@ }, { "id": "components-timeline-features--with-inline-links" + }, + { + "id": "components-timeline-features--with-actions" } ], "importPath": "@primer/react", @@ -65,6 +68,10 @@ { "name": "Timeline.Break", "props": [] + }, + { + "name": "Timeline.Actions", + "props": [] } ] -} +} \ No newline at end of file diff --git a/packages/react/src/Timeline/Timeline.features.stories.module.css b/packages/react/src/Timeline/Timeline.features.stories.module.css index f7da0223915..63d8cac33ad 100644 --- a/packages/react/src/Timeline/Timeline.features.stories.module.css +++ b/packages/react/src/Timeline/Timeline.features.stories.module.css @@ -7,3 +7,112 @@ .LinkWithBoldStyle:hover { color: var(--fgColor-accent); } + +.RealisticTimeline { + /* GitHub renders the timeline at most 1012px wide in product surfaces. + Constrain the story width to match so Timeline.Actions content sits a + realistic distance from the event text. */ + max-width: 1012px; +} + +.ShaLink { + font-family: var(--fontStack-monospace); + font-size: var(--text-body-size-small); + text-decoration: underline; +} + +.CommitSha { + font-family: var(--fontStack-monospace); + font-size: var(--text-body-size-small); + font-weight: var(--base-text-weight-semibold); + color: var(--fgColor-default); +} + +.SignatureLabelVerified { + /* Verified signatures on github.com keep the default muted Label border + but render the text in success green. Label variant="success" would + also color the border, which is louder than the real product. */ + color: var(--fgColor-success); +} + +.IconSuccess { + color: var(--fgColor-success); +} + +.IconDanger { + color: var(--fgColor-danger); +} + +.ChecksSubline { + margin-top: var(--base-size-4); + font-size: var(--text-body-size-small); + color: var(--fgColor-muted); +} + +.CrossReferenceRow { + display: flex; + align-items: flex-start; + gap: var(--base-size-12); + margin-top: var(--base-size-4); +} + +.CrossReferenceContent { + flex: 1; + min-width: 0; +} + +.CrossReferenceTitle { + font-size: var(--text-title-size-small); + line-height: var(--text-title-lineHeight-small); +} + +.CrossReferenceLink { + color: var(--fgColor-default); + text-decoration: underline; +} + +.CrossReferenceLink:hover { + color: var(--fgColor-accent); +} + +.CrossReferenceName { + font-weight: var(--base-text-weight-semibold); +} + +.CrossReferenceNumber { + color: var(--fgColor-muted); +} + +.CrossReferenceLink:hover .CrossReferenceNumber { + color: var(--fgColor-accent); +} + +.CrossReferenceMeta { + color: var(--fgColor-muted); + flex-shrink: 0; +} + +.CrossReferenceActions { + display: flex; + align-items: center; + gap: var(--base-size-12); + flex-shrink: 0; +} + +.CrossReferenceTaskline { + display: flex; + align-items: center; + gap: var(--base-size-4); + margin-top: var(--base-size-4); + font-size: var(--text-body-size-small); + color: var(--fgColor-muted); +} + +.InlineAvatar { + vertical-align: middle; + margin-right: var(--base-size-4); +} + +.Timestamp { + text-decoration: underline; +} diff --git a/packages/react/src/Timeline/Timeline.features.stories.tsx b/packages/react/src/Timeline/Timeline.features.stories.tsx index 34875f3a739..05380f6dc43 100644 --- a/packages/react/src/Timeline/Timeline.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.features.stories.tsx @@ -3,6 +3,8 @@ import type {ComponentProps} from '../utils/types' import Timeline from './Timeline' import Octicon from '../Octicon' import { + CheckIcon, + CrossReferenceIcon, FlameIcon, GitBranchIcon, GitCommitIcon, @@ -11,10 +13,18 @@ import { HeartIcon, IssueClosedIcon, IssueOpenedIcon, + LockIcon, + RepoPushIcon, SkipIcon, + TasklistIcon, XIcon, } from '@primer/octicons-react' import Link from '../Link' +import {Button} from '../Button' +import Label from '../Label' +import StateLabel from '../StateLabel' +import Avatar from '../Avatar' +import BranchName from '../BranchName' import classes from './Timeline.features.stories.module.css' export default { @@ -25,6 +35,7 @@ export default { 'Timeline.Badge': Timeline.Badge, 'Timeline.Body': Timeline.Body, 'Timeline.Break': Timeline.Break, + 'Timeline.Actions': Timeline.Actions, }, } as Meta> @@ -201,3 +212,134 @@ export const WithInlineLinks = () => ( ) + +export const WithActions = () => ( +
{ + if ((e.target as HTMLElement).closest('a')) e.preventDefault() + }} + > + + + + + + + + Monalisa + + merged via the queue into main with commit{' '} + + 01e49tb + {' '} + + just now + +
28 checks passed
+
+ + + + +
+ + + + + + + Monalisa + + force-pushed the main branch from{' '} + + 01e49tb + {' '} + to{' '} + + 02f50uc + {' '} + + 2 hours ago + + + + + + + + + + + + + Update README.md + + + + + + + 3fbdc0 + + + + + + + + + + Initial commit + + + + + + + 3fbdc0 + + + + + + + + + + + Monalisa + + mentioned this pull request{' '} + + just now + +
+
+
+ + Fix positioning of Autocomplete overlay menu{' '} + primer/react#7431 + +
+
+ + 17 tasks +
+
+
+ + + Open + +
+
+
+
+
+
+) diff --git a/packages/react/src/Timeline/Timeline.module.css b/packages/react/src/Timeline/Timeline.module.css index f4bdc114c8b..6cdf87d9956 100644 --- a/packages/react/src/Timeline/Timeline.module.css +++ b/packages/react/src/Timeline/Timeline.module.css @@ -155,3 +155,14 @@ margin-bottom: calc(-1 * var(--base-size-12)); } } + +.TimelineItemActions { + display: flex; + align-items: center; + align-self: flex-start; + /* Match the badge wrapper height (32px in both default and condensed) so contents vertically center against the badge */ + min-height: var(--base-size-32); + margin-left: auto; + gap: var(--base-size-8); + flex-shrink: 0; +} diff --git a/packages/react/src/Timeline/Timeline.stories.tsx b/packages/react/src/Timeline/Timeline.stories.tsx index fd03321f503..977bccedb65 100644 --- a/packages/react/src/Timeline/Timeline.stories.tsx +++ b/packages/react/src/Timeline/Timeline.stories.tsx @@ -12,6 +12,7 @@ export default { 'Timeline.Badge': Timeline.Badge, 'Timeline.Body': Timeline.Body, 'Timeline.Break': Timeline.Break, + 'Timeline.Actions': Timeline.Actions, }, } as Meta> diff --git a/packages/react/src/Timeline/Timeline.tsx b/packages/react/src/Timeline/Timeline.tsx index 8abec1e5d99..b5a08d665ef 100644 --- a/packages/react/src/Timeline/Timeline.tsx +++ b/packages/react/src/Timeline/Timeline.tsx @@ -100,9 +100,21 @@ const TimelineBreak = React.forwardRef(({cla TimelineBreak.displayName = 'TimelineBreak' +export type TimelineActionsProps = { + /** Class name for custom styling */ + className?: string +} & React.ComponentPropsWithoutRef<'div'> + +const TimelineActions = React.forwardRef(({className, ...props}, forwardRef) => { + return
+}) + +TimelineActions.displayName = 'Timeline.Actions' + export default Object.assign(Timeline, { Item: TimelineItem, Badge: TimelineBadge, Body: TimelineBody, Break: TimelineBreak, + Actions: TimelineActions, }) diff --git a/packages/react/src/Timeline/__tests__/Timeline.test.tsx b/packages/react/src/Timeline/__tests__/Timeline.test.tsx index c4031c5a84c..bc5efaf6d1e 100644 --- a/packages/react/src/Timeline/__tests__/Timeline.test.tsx +++ b/packages/react/src/Timeline/__tests__/Timeline.test.tsx @@ -72,3 +72,23 @@ describe('Timeline.Body', () => { describe('Timeline.Break', () => { implementsClassName(Timeline.Break, classes.TimelineBreak) }) + +describe('Timeline.Actions', () => { + implementsClassName(Timeline.Actions, classes.TimelineItemActions) + + it('renders children', () => { + const {getByTestId} = render( + + + , + ) + expect(getByTestId('actions-child')).toBeInTheDocument() + }) + + it('forwards additional props to the underlying element', () => { + const {container} = render() + expect(container.firstChild).toHaveAttribute('data-foo', 'bar') + }) +}) diff --git a/packages/react/src/Timeline/index.ts b/packages/react/src/Timeline/index.ts index e1a60723c81..dac4162fe4b 100644 --- a/packages/react/src/Timeline/index.ts +++ b/packages/react/src/Timeline/index.ts @@ -7,4 +7,5 @@ export type { TimelineBadgeProps, TimelineBodyProps, TimelineBreakProps, + TimelineActionsProps, } from './Timeline' diff --git a/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap index da0293203ad..3a5f5d1f16b 100644 --- a/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap @@ -192,6 +192,7 @@ exports[`@primer/react > should not update exports without a semver change 1`] = "type ThemeProviderProps", "ThemeShadowPaths", "Timeline", + "type TimelineActionsProps", "type TimelineBadgeProps", "type TimelineBadgeVariant", "type TimelineBodyProps", diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 8fd9d2e8ee9..370f1b507ab 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -183,6 +183,7 @@ export type {TextProps} from './Text' export {default as Timeline} from './Timeline' export type { TimelineProps, + TimelineActionsProps, TimelineBadgeVariant, TimelineBadgeProps, TimelineBodyProps,