diff --git a/packages/react/src/Timeline/Timeline.comments.features.stories.module.css b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css new file mode 100644 index 00000000000..cde52fe2bc5 --- /dev/null +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css @@ -0,0 +1,237 @@ +.RealisticTimeline { + /* GitHub renders the timeline at most 1012px wide in product surfaces. Reserve the + ~72px gutter so the large avatar (which sits left of the rail) isn't clipped. */ + max-width: 1012px; + padding-left: calc(var(--base-size-40) + var(--base-size-32)); +} + +/* Seat the 40px gutter avatar to the LEFT of the rail (left edge ~72px left of the + rail), so its right edge lands just left of the rail and the card overlaps the + rail — matching Figma: avatar-left → card-left ≈ 56px, caret bridges the gap. The + default Timeline.Avatar `top` / translateY (vertical centering) is preserved. + Specificity (.RealisticTimeline .GutterAvatar = 0-2-0) beats the base + .TimelineItemAvatar (0-1-0) regardless of stylesheet order. */ +.RealisticTimeline .GutterAvatar { + left: calc(-1 * (var(--base-size-40) + var(--base-size-32))); +} + +/* Story-only scaffolding: each variant is wrapped in its own
with a + small caption heading ABOVE the card, mirroring the badge-row Issue stories so + the card itself renders exactly as it would in product. Not part of the card. */ +.Variant { + margin-bottom: var(--base-size-24); +} + +.VariantLabel { + margin: 0 0 var(--base-size-8); + font-size: var(--text-body-size-small); + font-weight: var(--base-text-weight-semibold); + color: var(--fgColor-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* The bordered comment card. Mirrors GitHub's `.timeline-comment` shell: a + 1px-bordered, rounded box that the gutter avatar's speech-bubble caret points + at. There is no Primer comment primitive, so the box is composed directly. + `margin-left` pushes the card to the RIGHT of the on-rail gutter avatar so the + rail stays in the gutter (behind the avatar) and never passes behind the opaque + card; the caret bridges the small remaining gap. */ +.Card { + position: relative; + flex: auto; + min-width: 0; + margin-left: calc(-1 * var(--base-size-16)); + border: var(--borderWidth-thin) solid var(--borderColor-default); + border-radius: var(--borderRadius-medium); + background-color: var(--bgColor-default); +} + +/* Speech-bubble caret pointing from the card's left edge toward the gutter + avatar, recreated with the classic two-triangle border trick (outer border + triangle + inner background triangle). The border declarations below describe + triangle GEOMETRY, not a real border, so the raw px/color values are + intentional and the primer size/border/color rules are disabled for them. */ +/* stylelint-disable primer/spacing, primer/borders, primer/colors -- CSS triangle geometry, not a real border */ +.Card::before { + position: absolute; + top: 12px; + left: -8px; + display: block; + width: 0; + height: 0; + content: ''; + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-right: 8px solid var(--borderColor-default); +} + +.Card::after { + position: absolute; + top: 13px; + left: -7px; + display: block; + width: 0; + height: 0; + content: ''; + border-top: 7px solid transparent; + border-bottom: 7px solid transparent; + border-right: 7px solid var(--bgColor-muted); +} +/* stylelint-enable primer/spacing, primer/borders, primer/colors */ + +/* Header bar: muted background, bottom divider, rounded top corners — matches + github-ui's `ActivityHeader` `containerBase`. Holds the author identity row on + the left and the actions affordance on the right. */ +.CardHeader { + display: flex; + align-items: center; + gap: var(--base-size-8); + padding: var(--base-size-8) var(--base-size-16); + background-color: var(--bgColor-muted); + border-bottom: var(--borderWidth-thin) solid var(--borderColor-default); + border-top-left-radius: var(--borderRadius-medium); + border-top-right-radius: var(--borderRadius-medium); +} + +.HeaderText { + display: flex; + flex-wrap: wrap; + align-items: baseline; + column-gap: var(--base-size-4); + min-width: 0; + flex: auto; +} + +/* Bold author link. Bold weight (not just muted color) keeps the in-text link + above the high-contrast axe threshold per the a11y in-text-link rule. */ +.AuthorLink { + font-weight: var(--base-text-weight-semibold); + color: var(--fgColor-default); +} + +.AuthorLink:hover { + color: var(--fgColor-accent); +} + +/* The timestamp permalink plus any " – with {app}" via-app suffix. Kept as plain + inline text (NOT flex) so the whitespace around the suffix renders naturally. */ +.TimestampLine { + color: var(--fgColor-muted); +} + +/* Muted, underlined relative-time permalink. The underline keeps this muted + in-text link high-contrast accessible (matches the badge-row `.Timestamp`). */ +.Timestamp { + text-decoration: underline; +} + +.CardActions { + display: flex; + align-items: center; + flex-shrink: 0; + margin-left: auto; +} + +/* Body: the rendered markdown. Padding mirrors github-ui's `IssueCommentBody` + (16px around the content). */ +.CardBody { + padding: var(--base-size-16); + font-size: var(--text-body-size-medium); + color: var(--fgColor-default); +} + +.CardBody p { + margin: 0; +} + +.CardBody p:not(:first-child) { + margin-top: var(--base-size-8); +} + +.CardBody code { + padding: var(--base-size-2) var(--base-size-4); + font-family: var(--fontStack-monospace); + font-size: var(--text-body-size-small); + background-color: var(--bgColor-neutral-muted); + border-radius: var(--borderRadius-medium); +} + +/* Reactions row below the body — GitHub renders these directly under the body text + with no separating divider. Each reaction is a small pill. */ +.Reactions { + display: flex; + gap: var(--base-size-8); + padding: 0 var(--base-size-16) var(--base-size-16); +} + +.Reaction { + display: inline-flex; + align-items: center; + gap: var(--base-size-4); + padding: 0 var(--base-size-8); + height: var(--base-size-24); + font-size: var(--text-body-size-small); + color: var(--fgColor-muted); + background-color: var(--bgColor-muted); + border: var(--borderWidth-thin) solid var(--borderColor-default); + border-radius: var(--borderRadius-full); + cursor: pointer; +} + +.Reaction:hover { + background-color: var(--bgColor-accent-muted); + border-color: var(--borderColor-accent-muted); +} + +.ReactionCount { + font-weight: var(--base-text-weight-semibold); + color: var(--fgColor-default); +} + +/* Octicon avatar mode: a 40px container with a centered octicon, used where the + actor is represented by an icon rather than a photo. Base = CIRCLE + subtle muted + background + muted icon (e.g. Copilot). Sits in the same gutter slot as the photo + avatar. Shape/tone modifiers below cover the other presets. */ +.OcticonAvatar { + display: flex; + align-items: center; + justify-content: center; + width: var(--base-size-40); + height: var(--base-size-40); + color: var(--fgColor-muted); + background-color: var(--bgColor-muted); + border: var(--borderWidth-thin) solid var(--borderColor-default); + border-radius: var(--borderRadius-full); +} + +/* Rounded-square variant (e.g. Dependabot), matching the brand avatar shape. */ +.OcticonAvatarSquare { + border-radius: var(--borderRadius-medium); +} + +/* Accent (blue) tone with a white icon — approximates the Dependabot brand-blue + avatar (accent-emphasis is the closest Primer token to the Dependabot blue). */ +.OcticonAvatarAccent { + color: var(--fgColor-onEmphasis); + background-color: var(--bgColor-accent-emphasis); + border-color: var(--borderColor-accent-emphasis); +} + +/* Threaded reply (wired for the deferred reply stories): the nested card drops its + border + speech-bubble caret + gutter offset so replies read as part of the + parent thread (no on-rail avatar to clear). */ +.CardReply { + margin-left: 0; + border: 0; +} + +.CardReply::before, +.CardReply::after { + display: none; +} + +.CardReply .CardHeader { + border-top-left-radius: 0; + border-top-right-radius: 0; +} diff --git a/packages/react/src/Timeline/Timeline.comments.features.stories.tsx b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx new file mode 100644 index 00000000000..9a382b8388f --- /dev/null +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx @@ -0,0 +1,361 @@ +import type {Meta} from '@storybook/react-vite' +import type React from 'react' +import {clsx} from 'clsx' +import type {ComponentProps} from '../utils/types' +import {FeatureFlags} from '../FeatureFlags' +import Timeline from './Timeline' +import {CopilotIcon, DependabotIcon, KebabHorizontalIcon, SmileyIcon} from '@primer/octicons-react' +import Avatar from '../Avatar' +import {IconButton} from '../Button' +import Label from '../Label' +import Link from '../Link' +import RelativeTime from '../RelativeTime' +import classes from './Timeline.comments.features.stories.module.css' + +/** + * Issue Timeline COMMENT examples (Phase 2 of github/primer#6663). + * + * These stories recreate GitHub's issue-timeline comment card using Primer React + * primitives, sourced from the live React implementation in `github/github-ui` + * (`packages/commenting/components/issue-comment/IssueComment.tsx`, + * `IssueCommentViewer.tsx`, `ActivityHeader.tsx`, `CommentAuthorAssociation.tsx`, + * `CommentSubjectAuthor.tsx`). + * + * TITLE / IA: These live under `Components/Timeline/Events/Issues` — the same node + * as the badge-row Issue stories — even though a comment is a bordered CARD rather + * than a one-line badge row. We keep the per-SURFACE nesting decision: shared + * events' Issue-sourced versions live in the Issue folder; the PR-sourced comment + * version (ERB implementation) will land later under a PR folder. The filename + * (`Timeline.comments.features.stories.tsx`) does not drive Storybook IA — the + * `title` does — so comment stories nest beside the badge rows from a separate file. + * + * SCOPE: Storybook-only by design, like the Issue badge-row stories. They are + * intentionally NOT wired into components-json / the primer.style docs page (do NOT + * add this file to `Timeline.docs.json` or `build.ts`). Individual timeline events + * are not consumer-facing components. + * + * FAITHFULNESS: There is no Primer "Comment" primitive, so the card is composed + * directly via the local `CommentCard` helper below (header bar + body + reactions + + * speech-bubble caret). The Timeline rail + gutter avatar come from `Timeline.Item` + + * `Timeline.Avatar`. The goal is a clean, recognizable GitHub comment card, not + * pixel-perfection. + * + * VERIFIED LIVE DENOTATIONS (`ActivityHeader` + labels): + * - Author-of-issue badge: text "Author" (`LABELS.commentAuthor`), `Label` variant + * `secondary`, shown when the commenter opened the issue (`CommentSubjectAuthor`). + * - Author-association badge (Member/Owner/Collaborator/Contributor): variant + * `secondary` (`CommentAuthorAssociation`). + * - Bot/AI badge: `LABELS.authorLabel(isBot, isCopilot)` → "bot" for bots, "AI" for + * Copilot, "mannequin" for mannequins; `Label` variant `secondary`, next to the name. + * - Copilot: name renders as "Copilot"; the 40px GUTTER avatar is a muted `CopilotIcon` + * octicon in a circle (audit: "Copilot = octicon avatar"). The live `square={isCopilot}` + * applies to the small 24px HEADER avatar, a different element from this gutter avatar. + * - via-app: the timestamp line gets a " – with {app}" suffix, the app name an + * `inline` (underlined) `Link` — see the via-app section. + * + * AVATAR SHAPE (resolved matrix): + * - Users → CIRCLE photo avatar. + * - Bot (github-actions) → SQUARE photo avatar. Source-of-truth precedence is "what + * renders on github.com" > literal component behavior: bot/app accounts have square + * avatars by ACCOUNT TYPE, set upstream of the comment component, so the live result + * is square. The React `ActivityHeader` only FORCES `square={isCopilot}`, which + * avoids overriding an already-square bot avatar; it does NOT make bots circular. + * - Copilot → OCTICON avatar: a muted `CopilotIcon` in a 40px CIRCLE with a subtle + * muted background (audit "Copilot = octicon avatar"). The earlier "square Copilot" + * note referred to the live 24px header avatar, not this 40px gutter avatar. + * - Dependabot → OCTICON avatar (per Figma spec): a white `DependabotIcon` in a 40px + * ROUNDED-SQUARE with an accent-blue background — the clean Dependabot brand avatar. + * `bgColor-accent-emphasis` is the closest Primer token to the Dependabot brand blue. + * (Replaces the old photo URL, which rendered an off-brand hexagonal design.) + */ + +const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4' +// github-actions[bot] (u/44036562) — a representative generic GitHub App bot. +const GITHUB_ACTIONS_AVATAR = 'https://avatars.githubusercontent.com/u/44036562?v=4' + +type CommentCardProps = { + /** Display name of the comment author (rendered as a bold link). */ + authorName: string + authorHref?: string + /** Photo avatar URL. Omit when using `avatarIcon` (e.g. Copilot). */ + avatarSrc?: string + /** Circle for users; square for bots/Dependabot photo avatars. Ignored when `avatarIcon` is set. */ + avatarShape?: 'circle' | 'square' + /** + * Octicon avatar mode: render this icon inside a 40px container instead of a photo + * `Avatar` (Copilot, Dependabot). Pair with `avatarIconShape` / `avatarIconTone`. + */ + avatarIcon?: React.ElementType + /** Octicon-avatar container shape (default 'circle'; 'square' for Dependabot). */ + avatarIconShape?: 'circle' | 'square' + /** Octicon-avatar tone: 'muted' (subtle gray, Copilot) or 'accent' (blue + white icon, Dependabot). */ + avatarIconTone?: 'muted' | 'accent' + /** "Author"/"Member"/"Owner"/… subject-author or association badge (variant secondary). */ + associationLabel?: string + associationAriaLabel?: string + /** "bot"/"AI"/"mannequin" actor badge shown next to the name (variant secondary). */ + badgeLabel?: string + badgeAriaLabel?: string + /** ISO timestamp for the relative-time permalink. */ + timestamp: string + /** Renders the live " – with {app}" suffix in the timestamp line. */ + viaApp?: {name: string; href?: string} + /** Show the reactions footer. */ + reactions?: boolean + /** + * Threaded reply: drops the card border + speech-bubble caret + gutter avatar. + * Wired now for the deferred threaded-reply stories; no reply story ships yet. + */ + isReply?: boolean + children: React.ReactNode +} + +/** + * Self-contained comment-card composition (NOT exported — local to this file, like + * the badge-row stories' `Actor`/`Time` helpers). Renders the full `Timeline.Item`: + * the gutter `Timeline.Avatar` (40px photo or octicon) seated ON the rail, and the + * bordered card (header bar with author + badges + timestamp + actions, markdown body, + * optional reactions). The speech-bubble caret (CSS) bridges the avatar to the card. + */ +const CommentCard = ({ + authorName, + authorHref = '#', + avatarSrc, + avatarShape = 'circle', + avatarIcon: AvatarIcon, + avatarIconShape = 'circle', + avatarIconTone = 'muted', + associationLabel, + associationAriaLabel, + badgeLabel, + badgeAriaLabel, + timestamp, + viaApp, + reactions = false, + isReply = false, + children, +}: CommentCardProps) => ( + + {!isReply && ( + + {AvatarIcon ? ( + + + + ) : ( + + )} + + )} +
+
+
+ {/* Bold (not just muted) keeps the author link above the high-contrast axe + threshold per the a11y in-text-link rule. */} + + {authorName} + + {badgeLabel ? ( + + ) : null} + {associationLabel ? ( + + ) : null} + + {/* Muted + underlined keeps this muted permalink high-contrast accessible. */} + + + + {viaApp ? ( + <> + {' – with '} + {/* `inline` (underline) satisfies the a11y in-text-link rule. */} + + {viaApp.name} + + + ) : null} + +
+
+ +
+
+
{children}
+ {reactions ? ( +
+ + + +
+ ) : null} +
+
+) + +/** + * Story-only scaffolding: a captioned `
` wrapping a single ``, + * mirroring the badge-row Issue stories so each card renders as it would in product. + */ +const CommentSection = ({label, children}: {label: string; children: React.ReactNode}) => ( +
+

{label}

+ {children} +
+) + +/** + * The Comment event group — all actor variants of a timeline comment card, stacked in + * one export so they can be scanned like a Figma component set (matching the badge-row + * stories' "one export per event group" pattern). Each `
` is captioned and + * holds a single `CommentCard`. Deferred (NOT shown): threaded review replies, + * embedded-in-thread comments, minimized/collapsed states — the `CommentCard` helper's + * `isReply` prop is wired for those later. + */ +export const EventComment = () => ( +
{ + if (e.target instanceof Element && e.target.closest('a')) e.preventDefault() + }} + > + {/* Standard USER comment: circular photo avatar, the "Author" subject-author badge + (the commenter opened the issue), a muted relative-time permalink, and reactions. */} + + +

+ Thanks for the report! I can reproduce this with npm run build on a clean checkout. Looks like + the regression landed in{' '} + + #1234 + {' '} + — I'll open a fix shortly. +

+
+
+ + {/* Bot comment (e.g. github-actions): live `ActivityHeader` renders the actor badge + as "bot" (`LABELS.authorLabel`). */} + + +

+ All checks have passed ✅ — build, test, and lint are green on the + latest commit. +

+
+
+ + {/* Copilot comment: name renders as "Copilot", the actor badge is "AI" + (`LABELS.authorLabel(true, true)`). The 40px gutter avatar is a muted CopilotIcon + octicon in a circle (audit "Copilot = octicon avatar"); the live square={isCopilot} + applies to the separate 24px header avatar, not this gutter avatar. */} + + +

+ I've analyzed the failing test. The assertion in{' '} + + parser.test.ts + {' '} + expects the old token shape — updating the fixture should resolve it. +

+
+
+ + {/* Dependabot comment: live `ActivityHeader` renders the actor badge as "bot". */} + + +

+ Bumps lodash from 4.17.20 to 4.17.21. This update includes a security fix —{' '} + + view the advisory + + . +

+
+
+ + {/* User comment via a GitHub App: the timestamp line gains a " – with {app}" suffix, + the app name an `inline` (underlined) `Link`. Live adds no child/app avatar here. */} + + +

Mirrored from our internal tracker — closing the loop here so the thread stays in sync.

+
+
+
+) + +export default { + title: 'Components/Timeline/Events/Issues', + component: Timeline, + subcomponents: { + 'Timeline.Item': Timeline.Item, + 'Timeline.Avatar': Timeline.Avatar, + 'Timeline.Body': Timeline.Body, + }, + decorators: [ + // File-scoped: render every story in the future-state list semantics + // (`
    `/`
  1. `). The `primer_react_timeline_list_semantics` flag is merged + // on main; this opts these stories into the DOM the timeline will ship. + Story => ( + + + + ), + ], +} as Meta>