From faa08b6568884790def26570cfed4ca09bb911ad Mon Sep 17 00:00:00 2001 From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:15:51 -0700 Subject: [PATCH 1/7] Add Timeline comment-card proof-of-pattern story (CommentStandard) Storybook-only example recreating GitHub's issue-timeline comment card from Primer React primitives (Avatar, Link, Label, RelativeTime, IconButton) under Components/Timeline/Events/Comments. Not wired into Timeline.docs.json. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...eline.comments.features.stories.module.css | 172 ++++++++++++++++++ .../Timeline.comments.features.stories.tsx | 136 ++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 packages/react/src/Timeline/Timeline.comments.features.stories.module.css create mode 100644 packages/react/src/Timeline/Timeline.comments.features.stories.tsx 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..5991c6f0748 --- /dev/null +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css @@ -0,0 +1,172 @@ +.RealisticTimeline { + /* GitHub renders the timeline at most 1012px wide in product surfaces. The + comment card sits in the same rail as badge-row events, with the 40px actor + avatar in the left gutter, so reserve that gutter and cap the width to match. */ + max-width: 1012px; + padding-left: calc(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. */ +.Card { + position: relative; + flex: auto; + min-width: 0; + 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); +} + +/* 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, separated by a divider β€” mirrors the reaction + bar GitHub renders under a comment. Each reaction is a small pill. */ +.Reactions { + display: flex; + gap: var(--base-size-8); + padding: var(--base-size-8) var(--base-size-16); + border-top: var(--borderWidth-thin) solid var(--borderColor-muted); +} + +.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); +} 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..bb7b5c0957b --- /dev/null +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx @@ -0,0 +1,136 @@ +import type {Meta} from '@storybook/react-vite' +import type {ComponentProps} from '../utils/types' +import {FeatureFlags} from '../FeatureFlags' +import Timeline from './Timeline' +import {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`, `CommentHeader.tsx`, + * `CommentAuthorAssociation.tsx`). + * + * TITLE CHOICE: These live under `Components/Timeline/Events/Comments` rather than + * `.../Events/Issues`. A comment is a bordered CARD (header bar + body + reactions), + * not a one-line badge row like the Issue events, and the comment surface is SHARED + * between Issue and Pull Request timelines (via different implementations β€” Issue = + * React `IssueComment`; PR = ERB). Giving comments their own folder keeps the badge + * rows clean and leaves room for the PR-sourced variant later. + * + * 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 from `Avatar`, `Link`, `Label`, `RelativeTime`, `IconButton`, and a + * bordered container. The Timeline rail + gutter avatar come from `Timeline.Item` + + * `Timeline.Avatar` (the comment uses the gutter avatar, not a `Timeline.Badge`). + * The goal is a clean, recognizable GitHub comment card, not pixel-perfection. + */ + +const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4' + +/** + * A single standard USER comment card (proof-of-pattern). Structure, top to bottom: + * + * - Gutter: a 40px circular `Avatar` placed in `Timeline.Avatar`, sitting on the + * rail. The card's speech-bubble caret (CSS) points back at it. + * - Header bar (`bgColor-muted`, bottom divider): bold author `Link` + an "Author" + * author-association `Label` (`secondary`) + a muted, underlined relative-time + * permalink `Link`, with a kebab `IconButton` actions affordance on the right. + * - Body: a short markdown-style paragraph (with inline `` and an inline + * ``). + * - Reactions: a simple row of emoji pills with counts. + * + * In-text links are bold (author) or underlined (timestamp, inline body link) so + * they clear the high-contrast axe threshold per the a11y in-text-link rule. + */ +export const CommentStandard = () => ( +
{ + if ((e.target as HTMLElement).closest('a')) e.preventDefault() + }} + > +
+

Standard user comment

+ + + + + +
+
+
+ + monalisa + + + + + +
+
+ +
+
+
+

+ 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. +

+
+
+ + + +
+
+
+
+
+
+) + +export default { + title: 'Components/Timeline/Events/Comments', + 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> From 023b86c1d1dee874fe37d82111fc5644ee59e08d Mon Sep 17 00:00:00 2001 From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:35:49 -0700 Subject: [PATCH 2/7] Add Timeline comment actor variants (Bot, Copilot, Dependabot, ViaApp) Move the comment stories under Components/Timeline/Events/Issues, extract a local CommentCard helper, and add bot/Copilot/Dependabot/via-app variants recreated from the live github-ui ActivityHeader. Storybook-only; isReply prop wired for deferred threaded replies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...eline.comments.features.stories.module.css | 22 ++ .../Timeline.comments.features.stories.tsx | 356 ++++++++++++++---- 2 files changed, 303 insertions(+), 75 deletions(-) diff --git a/packages/react/src/Timeline/Timeline.comments.features.stories.module.css b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css index 5991c6f0748..362739f14b5 100644 --- a/packages/react/src/Timeline/Timeline.comments.features.stories.module.css +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css @@ -101,6 +101,12 @@ 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 { @@ -170,3 +176,19 @@ font-weight: var(--base-text-weight-semibold); color: var(--fgColor-default); } + +/* Threaded reply (wired for the deferred reply stories): the nested card drops its + border + speech-bubble caret so replies read as part of the parent thread. */ +.CardReply { + 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 index bb7b5c0957b..3228a20e558 100644 --- a/packages/react/src/Timeline/Timeline.comments.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx @@ -1,4 +1,6 @@ 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' @@ -16,15 +18,16 @@ import classes from './Timeline.comments.features.stories.module.css' * 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`, `CommentHeader.tsx`, - * `CommentAuthorAssociation.tsx`). + * `IssueCommentViewer.tsx`, `ActivityHeader.tsx`, `CommentAuthorAssociation.tsx`, + * `CommentSubjectAuthor.tsx`). * - * TITLE CHOICE: These live under `Components/Timeline/Events/Comments` rather than - * `.../Events/Issues`. A comment is a bordered CARD (header bar + body + reactions), - * not a one-line badge row like the Issue events, and the comment surface is SHARED - * between Issue and Pull Request timelines (via different implementations β€” Issue = - * React `IssueComment`; PR = ERB). Giving comments their own folder keeps the badge - * rows clean and leaves room for the PR-sourced variant later. + * 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 @@ -32,91 +35,294 @@ import classes from './Timeline.comments.features.stories.module.css' * are not consumer-facing components. * * FAITHFULNESS: There is no Primer "Comment" primitive, so the card is composed - * directly from `Avatar`, `Link`, `Label`, `RelativeTime`, `IconButton`, and a - * bordered container. The Timeline rail + gutter avatar come from `Timeline.Item` + - * `Timeline.Avatar` (the comment uses the gutter avatar, not a `Timeline.Badge`). - * The goal is a clean, recognizable GitHub comment card, not pixel-perfection. + * 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" and the avatar is SQUARE (`square={isCopilot}`). + * - via-app: the timestamp line gets a " – with {app}" suffix, the app name an + * `inline` (underlined) `Link` β€” see the CommentViaApp story. + * + * AUDIT-vs-LIVE DRIFT (flagged for review): the live React comment header squares + * the avatar for COPILOT ONLY (`square={isCopilot}`); generic bots and Dependabot + * would render with a CIRCLE avatar in that exact component. We render bot and + * Dependabot comment avatars as SQUARE here to match (a) GitHub product appearance, + * (b) the already-shipped Dependabot badge-row surface (ERB + * `ActorComponent`, square avatar + "bot" tag), and (c) this task's brief. If strict + * React-comment-path fidelity is preferred, switch those `avatarShape` props to + * 'circle'. */ const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4' +// dependabot[bot] (u/27347476) β€” same public avatar used on the Dependabot badge-row +// surface, kept for cross-surface consistency. +const DEPENDABOT_AVATAR = 'https://avatars.githubusercontent.com/u/27347476?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' +// Copilot (u/198982749) β€” renders SQUARE, matching the live `square={isCopilot}`. +const COPILOT_AVATAR = 'https://avatars.githubusercontent.com/u/198982749?v=4' + +type CommentCardProps = { + /** Display name of the comment author (rendered as a bold link). */ + authorName: string + authorHref?: string + avatarSrc: string + /** Circle for users; square for Copilot (live) and β€” per the drift note above β€” bots. */ + avatarShape?: 'circle' | 'square' + /** "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 +} /** - * A single standard USER comment card (proof-of-pattern). Structure, top to bottom: - * - * - Gutter: a 40px circular `Avatar` placed in `Timeline.Avatar`, sitting on the - * rail. The card's speech-bubble caret (CSS) points back at it. - * - Header bar (`bgColor-muted`, bottom divider): bold author `Link` + an "Author" - * author-association `Label` (`secondary`) + a muted, underlined relative-time - * permalink `Link`, with a kebab `IconButton` actions affordance on the right. - * - Body: a short markdown-style paragraph (with inline `` and an inline - * ``). - * - Reactions: a simple row of emoji pills with counts. - * - * In-text links are bold (author) or underlined (timestamp, inline body link) so - * they clear the high-contrast axe threshold per the a11y in-text-link rule. + * 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, circle or square) on the rail, and the bordered + * card (header bar with author + badges + timestamp + actions, markdown body, optional + * reactions). The speech-bubble caret (CSS) points from the card back at the avatar. */ -export const CommentStandard = () => ( +const CommentCard = ({ + authorName, + authorHref = '#', + avatarSrc, + avatarShape = 'circle', + associationLabel, + associationAriaLabel, + badgeLabel, + badgeAriaLabel, + timestamp, + viaApp, + reactions = false, + isReply = false, + children, +}: CommentCardProps) => ( + + {!isReply && ( + + + + )} +
    +
    +
    + {/* 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 the card itself renders as it would in + * product. The placeholder `href="#"` links are prevented from navigating in + * Storybook. + */ +const Variant = ({label, children}: {label: string; children: React.ReactNode}) => (
    { if ((e.target as HTMLElement).closest('a')) e.preventDefault() }} >
    -

    Standard user comment

    - - - - - -
    -
    -
    - - monalisa - - - - - -
    -
    - -
    -
    -
    -

    - 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. -

    -
    -
    - - - -
    -
    -
    -
    +

    {label}

    + {children}
    ) +/** + * A standard USER comment (proof-of-pattern): circular 40px avatar, the "Author" + * subject-author badge (the commenter opened the issue), a muted relative-time + * permalink, a short markdown-style body, and a reactions row. + */ +export const CommentStandard = () => ( + + +

    + 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. +

    +
    +
    +) + +/** + * A bot comment (e.g. github-actions). Live `ActivityHeader` renders the actor badge + * as "bot" (`LABELS.authorLabel`) next to the name. Avatar is SQUARE here per the + * drift note (live squares Copilot only). + */ +export const CommentBot = () => ( + + +

    + All checks have passed βœ… β€” build, test, and lint are green on the latest + commit. +

    +
    +
    +) + +/** + * A Copilot comment. Live denotation: the name renders as "Copilot", the actor badge + * is "AI" (`LABELS.authorLabel(true, true)`), and the avatar is SQUARE + * (`square={isCopilot}`) β€” the one square case confirmed directly in the live header. + */ +export const CommentCopilot = () => ( + + +

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

    +
    +
    +) + +/** + * A Dependabot comment. Live `ActivityHeader` renders the actor badge as "bot". Avatar + * is SQUARE here, matching the shipped Dependabot badge-row surface and product + * appearance (see drift note β€” the React comment header itself squares Copilot only). + */ +export const CommentDependabot = () => ( + + +

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

    +
    +
    +) + +/** + * A user comment posted via a GitHub App. Live denotation: the timestamp line gains a + * " – with {app}" suffix, the app name an `inline` (underlined) `Link`. Live does not + * add a child/app avatar in this path, so only the text suffix is shown. + */ +export const CommentViaApp = () => ( + + +

    Mirrored from our internal tracker β€” closing the loop here so the thread stays in sync.

    +
    +
    +) + export default { - title: 'Components/Timeline/Events/Comments', + title: 'Components/Timeline/Events/Issues', component: Timeline, subcomponents: { 'Timeline.Item': Timeline.Item, From e9fc3b5ba3c9e542064fead9f4281de3e10c2913 Mon Sep 17 00:00:00 2001 From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:39:58 -0700 Subject: [PATCH 3/7] Document square bot/Dependabot comment avatars as a resolved decision Capture the source-of-truth rationale (github.com renders bot/app avatars square by account type; ActivityHeader's square={isCopilot} doesn't force bots circular) as inline comments on CommentBot + CommentDependabot so reviewers don't revert to circle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Timeline.comments.features.stories.tsx | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/react/src/Timeline/Timeline.comments.features.stories.tsx b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx index 3228a20e558..dbc49fd929d 100644 --- a/packages/react/src/Timeline/Timeline.comments.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx @@ -51,14 +51,14 @@ import classes from './Timeline.comments.features.stories.module.css' * - via-app: the timestamp line gets a " – with {app}" suffix, the app name an * `inline` (underlined) `Link` β€” see the CommentViaApp story. * - * AUDIT-vs-LIVE DRIFT (flagged for review): the live React comment header squares - * the avatar for COPILOT ONLY (`square={isCopilot}`); generic bots and Dependabot - * would render with a CIRCLE avatar in that exact component. We render bot and - * Dependabot comment avatars as SQUARE here to match (a) GitHub product appearance, - * (b) the already-shipped Dependabot badge-row surface (ERB - * `ActorComponent`, square avatar + "bot" tag), and (c) this task's brief. If strict - * React-comment-path fidelity is preferred, switch those `avatarShape` props to - * 'circle'. + * AVATAR SHAPE (resolved decision): bot and Dependabot comment avatars are SQUARE. + * Source-of-truth precedence is "what renders on github.com" > literal component + * behavior. On github.com, bot/app accounts (github-actions, Dependabot) have square + * avatars by ACCOUNT TYPE β€” the shape is determined upstream of the comment component, + * so the live rendered result is square. The React `ActivityHeader` only FORCES + * `square={isCopilot}`, which avoids overriding an already-square bot avatar; it does + * NOT make bots render circular. Square also keeps Dependabot consistent with the + * shipped Dependabot badge-row surface (#8071). Users render CIRCLE; Copilot SQUARE. */ const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4' @@ -230,14 +230,22 @@ export const CommentStandard = () => ( /** * A bot comment (e.g. github-actions). Live `ActivityHeader` renders the actor badge - * as "bot" (`LABELS.authorLabel`) next to the name. Avatar is SQUARE here per the - * drift note (live squares Copilot only). + * as "bot" (`LABELS.authorLabel`) next to the name. + * + * Avatar is intentionally SQUARE. Source-of-truth precedence is "what renders on + * github.com" > literal component behavior: bot/app accounts have square avatars by + * account type (the shape is set upstream of the comment component), so the live + * rendered result is square. `ActivityHeader.tsx` only FORCES `square={isCopilot}`, + * which doesn't override an already-square bot avatar β€” it does not make bots + * circular. Do NOT "fix" this to 'circle'. */ export const CommentBot = () => ( ( ) /** - * A Dependabot comment. Live `ActivityHeader` renders the actor badge as "bot". Avatar - * is SQUARE here, matching the shipped Dependabot badge-row surface and product - * appearance (see drift note β€” the React comment header itself squares Copilot only). + * A Dependabot comment. Live `ActivityHeader` renders the actor badge as "bot". + * + * Avatar is intentionally SQUARE β€” same precedence as CommentBot: github.com renders + * Dependabot's avatar square by account type (set upstream of the comment component; + * `ActivityHeader.tsx`'s `square={isCopilot}` only avoids overriding it, it does not + * force bots circular). This also matches the shipped Dependabot badge-row surface + * (#8071), which uses the same square dependabot avatar. Do NOT "fix" this to 'circle'. */ export const CommentDependabot = () => ( Date: Sun, 28 Jun 2026 14:56:37 -0700 Subject: [PATCH 4/7] Fix comment rail/caret alignment, Copilot octicon avatar, consolidate stories - Seat the 40px gutter avatar ON the Timeline rail (rail passes behind the avatar, not behind the card) and push the card right via margin-left so the speech-bubble caret bridges avatar to card; drop the double left padding. - Add an octicon-avatar mode to CommentCard (avatarIcon prop) and render the Copilot gutter avatar as a muted CopilotIcon in a 40px circle. - Consolidate the five separate exports into a single EventComment story with the variants stacked in captioned sections. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...eline.comments.features.stories.module.css | 46 ++- .../Timeline.comments.features.stories.tsx | 313 +++++++++--------- 2 files changed, 194 insertions(+), 165 deletions(-) diff --git a/packages/react/src/Timeline/Timeline.comments.features.stories.module.css b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css index 362739f14b5..5170a25555a 100644 --- a/packages/react/src/Timeline/Timeline.comments.features.stories.module.css +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css @@ -1,9 +1,22 @@ .RealisticTimeline { - /* GitHub renders the timeline at most 1012px wide in product surfaces. The - comment card sits in the same rail as badge-row events, with the 40px actor - avatar in the left gutter, so reserve that gutter and cap the width to match. */ + /* GitHub renders the timeline at most 1012px wide in product surfaces. Just a + small left pad so the 40px gutter avatar (which sits ON the rail, see + .GutterAvatar) isn't clipped at the page edge β€” NOT a full avatar-column + reservation (the avatar overlays the gutter; the card is pushed right of it + via .Card's margin-left, so the two don't double-pad). */ max-width: 1012px; - padding-left: calc(var(--base-size-40) + var(--base-size-32)); + padding-left: var(--base-size-24); +} + +/* Re-seat the 40px gutter avatar so it is centered ON the Timeline rail (the + Timeline.Item ::before line), rather than pushed far into the negative gutter + like the badge-row avatars. This makes the rail pass BEHIND the avatar and lets + the speech-bubble caret bridge avatar β†’ card. The default Timeline.Avatar `top` + / translateY (vertical centering) is preserved; only `left` is overridden. + 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-20)); } /* Story-only scaffolding: each variant is wrapped in its own
    with a @@ -24,11 +37,15 @@ /* 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. */ + 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: var(--base-size-32); border: var(--borderWidth-thin) solid var(--borderColor-default); border-radius: var(--borderRadius-medium); background-color: var(--bgColor-default); @@ -177,9 +194,26 @@ color: var(--fgColor-default); } +/* Octicon avatar mode (e.g. Copilot): a 40px CIRCLE with a subtle muted background + and a centered muted octicon, used where the actor is represented by an icon + rather than a photo. Sits in the same gutter slot as the photo avatar. */ +.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); +} + /* Threaded reply (wired for the deferred reply stories): the nested card drops its - border + speech-bubble caret so replies read as part of the parent thread. */ + 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; } diff --git a/packages/react/src/Timeline/Timeline.comments.features.stories.tsx b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx index dbc49fd929d..9d646b6d57e 100644 --- a/packages/react/src/Timeline/Timeline.comments.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx @@ -4,7 +4,7 @@ import {clsx} from 'clsx' import type {ComponentProps} from '../utils/types' import {FeatureFlags} from '../FeatureFlags' import Timeline from './Timeline' -import {KebabHorizontalIcon, SmileyIcon} from '@primer/octicons-react' +import {CopilotIcon, KebabHorizontalIcon, SmileyIcon} from '@primer/octicons-react' import Avatar from '../Avatar' import {IconButton} from '../Button' import Label from '../Label' @@ -47,18 +47,25 @@ import classes from './Timeline.comments.features.stories.module.css' * `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" and the avatar is SQUARE (`square={isCopilot}`). + * - 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 CommentViaApp story. + * `inline` (underlined) `Link` β€” see the via-app section. * - * AVATAR SHAPE (resolved decision): bot and Dependabot comment avatars are SQUARE. - * Source-of-truth precedence is "what renders on github.com" > literal component - * behavior. On github.com, bot/app accounts (github-actions, Dependabot) have square - * avatars by ACCOUNT TYPE β€” the shape is determined upstream of the comment component, - * so the live rendered result is square. The React `ActivityHeader` only FORCES - * `square={isCopilot}`, which avoids overriding an already-square bot avatar; it does - * NOT make bots render circular. Square also keeps Dependabot consistent with the - * shipped Dependabot badge-row surface (#8071). Users render CIRCLE; Copilot SQUARE. + * AVATAR SHAPE (resolved decision): + * - Users render a CIRCLE photo avatar. + * - Copilot renders an OCTICON avatar: a muted `CopilotIcon` inside a 40px circle + * (see `avatarIcon` on `CommentCard`). Our earlier "square Copilot" note referred to + * the live 24px header avatar; the 40px gutter avatar here is the octicon. + * - Generic bots and Dependabot render a SQUARE photo avatar. Source-of-truth + * precedence is "what renders on github.com" > literal component behavior: bot/app + * accounts (github-actions, Dependabot) 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 render circular. Square also keeps + * Dependabot consistent with the shipped Dependabot badge-row surface (#8071). (The + * Dependabot avatar may switch to an octicon pending a forthcoming Figma spec.) */ const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4' @@ -67,16 +74,20 @@ const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4' const DEPENDABOT_AVATAR = 'https://avatars.githubusercontent.com/u/27347476?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' -// Copilot (u/198982749) β€” renders SQUARE, matching the live `square={isCopilot}`. -const COPILOT_AVATAR = 'https://avatars.githubusercontent.com/u/198982749?v=4' type CommentCardProps = { /** Display name of the comment author (rendered as a bold link). */ authorName: string authorHref?: string - avatarSrc: string - /** Circle for users; square for Copilot (live) and β€” per the drift note above β€” bots. */ + /** 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 muted circle instead of a + * photo `Avatar` (used for Copilot β€” audit "Copilot = octicon avatar"). + */ + avatarIcon?: React.ElementType /** "Author"/"Member"/"Owner"/… subject-author or association badge (variant secondary). */ associationLabel?: string associationAriaLabel?: string @@ -100,15 +111,16 @@ type CommentCardProps = { /** * 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, circle or square) on the rail, and the bordered - * card (header bar with author + badges + timestamp + actions, markdown body, optional - * reactions). The speech-bubble caret (CSS) points from the card back at the avatar. + * 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, associationLabel, associationAriaLabel, badgeLabel, @@ -121,8 +133,14 @@ const CommentCard = ({ }: CommentCardProps) => ( {!isReply && ( - - + + {AvatarIcon ? ( + + + + ) : ( + + )} )}
    @@ -183,156 +201,133 @@ const CommentCard = ({ /** * Story-only scaffolding: a captioned `
    ` wrapping a single ``, - * mirroring the badge-row Issue stories so the card itself renders as it would in - * product. The placeholder `href="#"` links are prevented from navigating in - * Storybook. + * 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. */ -const Variant = ({label, children}: {label: string; children: React.ReactNode}) => ( +export const EventComment = () => (
    { if ((e.target as HTMLElement).closest('a')) e.preventDefault() }} > -
    -

    {label}

    - {children} -
    -
    -) + {/* 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. +

    +
    +
    -/** - * A standard USER comment (proof-of-pattern): circular 40px avatar, the "Author" - * subject-author badge (the commenter opened the issue), a muted relative-time - * permalink, a short markdown-style body, and a reactions row. - */ -export const CommentStandard = () => ( - - -

    - 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. -

    -
    -
    -) - -/** - * A bot comment (e.g. github-actions). Live `ActivityHeader` renders the actor badge - * as "bot" (`LABELS.authorLabel`) next to the name. - * - * Avatar is intentionally SQUARE. Source-of-truth precedence is "what renders on - * github.com" > literal component behavior: bot/app accounts have square avatars by - * account type (the shape is set upstream of the comment component), so the live - * rendered result is square. `ActivityHeader.tsx` only FORCES `square={isCopilot}`, - * which doesn't override an already-square bot avatar β€” it does not make bots - * circular. Do NOT "fix" this to 'circle'. - */ -export const CommentBot = () => ( - - -

    - All checks have passed βœ… β€” build, test, and lint are green on the latest - commit. -

    -
    -
    -) + {/* 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. +

    +
    +
    -/** - * A Copilot comment. Live denotation: the name renders as "Copilot", the actor badge - * is "AI" (`LABELS.authorLabel(true, true)`), and the avatar is SQUARE - * (`square={isCopilot}`) β€” the one square case confirmed directly in the live header. - */ -export const CommentCopilot = () => ( - - -

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

    -
    -
    -) + {/* 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. +

    +
    +
    -/** - * A Dependabot comment. Live `ActivityHeader` renders the actor badge as "bot". - * - * Avatar is intentionally SQUARE β€” same precedence as CommentBot: github.com renders - * Dependabot's avatar square by account type (set upstream of the comment component; - * `ActivityHeader.tsx`'s `square={isCopilot}` only avoids overriding it, it does not - * force bots circular). This also matches the shipped Dependabot badge-row surface - * (#8071), which uses the same square dependabot avatar. Do NOT "fix" this to 'circle'. - */ -export const CommentDependabot = () => ( - - -

    - Bumps lodash from 4.17.20 to 4.17.21. This update includes a security fix β€”{' '} - - view the advisory - - . -

    -
    -
    -) + {/* 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 + + . +

    +
    +
    -/** - * A user comment posted via a GitHub App. Live denotation: the timestamp line gains a - * " – with {app}" suffix, the app name an `inline` (underlined) `Link`. Live does not - * add a child/app avatar in this path, so only the text suffix is shown. - */ -export const CommentViaApp = () => ( - - -

    Mirrored from our internal tracker β€” closing the loop here so the thread stays in sync.

    -
    -
    + {/* 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 { From e2871d7e43d2f7fa1c9abfb474db8206da681e72 Mon Sep 17 00:00:00 2001 From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com> Date: Sun, 28 Jun 2026 15:02:01 -0700 Subject: [PATCH 5/7] Use Dependabot brand octicon avatar (blue square, white DependabotIcon) Per the Figma spec, render the Dependabot comment gutter avatar as a white DependabotIcon on an accent-blue rounded square via the octicon-avatar mode (new avatarIconShape/avatarIconTone props), replacing the off-brand photo URL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...eline.comments.features.stories.module.css | 20 +++++- .../Timeline.comments.features.stories.tsx | 62 +++++++++++-------- 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/packages/react/src/Timeline/Timeline.comments.features.stories.module.css b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css index 5170a25555a..a5772b85349 100644 --- a/packages/react/src/Timeline/Timeline.comments.features.stories.module.css +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css @@ -194,9 +194,10 @@ color: var(--fgColor-default); } -/* Octicon avatar mode (e.g. Copilot): a 40px CIRCLE with a subtle muted background - and a centered muted octicon, used where the actor is represented by an icon - rather than a photo. Sits in the same gutter slot as the photo avatar. */ +/* 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; @@ -209,6 +210,19 @@ 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). */ diff --git a/packages/react/src/Timeline/Timeline.comments.features.stories.tsx b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx index 9d646b6d57e..f53569694a4 100644 --- a/packages/react/src/Timeline/Timeline.comments.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx @@ -4,7 +4,7 @@ import {clsx} from 'clsx' import type {ComponentProps} from '../utils/types' import {FeatureFlags} from '../FeatureFlags' import Timeline from './Timeline' -import {CopilotIcon, KebabHorizontalIcon, SmileyIcon} from '@primer/octicons-react' +import {CopilotIcon, DependabotIcon, KebabHorizontalIcon, SmileyIcon} from '@primer/octicons-react' import Avatar from '../Avatar' import {IconButton} from '../Button' import Label from '../Label' @@ -53,25 +53,23 @@ import classes from './Timeline.comments.features.stories.module.css' * - 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 decision): - * - Users render a CIRCLE photo avatar. - * - Copilot renders an OCTICON avatar: a muted `CopilotIcon` inside a 40px circle - * (see `avatarIcon` on `CommentCard`). Our earlier "square Copilot" note referred to - * the live 24px header avatar; the 40px gutter avatar here is the octicon. - * - Generic bots and Dependabot render a SQUARE photo avatar. Source-of-truth - * precedence is "what renders on github.com" > literal component behavior: bot/app - * accounts (github-actions, Dependabot) 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 render circular. Square also keeps - * Dependabot consistent with the shipped Dependabot badge-row surface (#8071). (The - * Dependabot avatar may switch to an octicon pending a forthcoming Figma spec.) + * 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' -// dependabot[bot] (u/27347476) β€” same public avatar used on the Dependabot badge-row -// surface, kept for cross-surface consistency. -const DEPENDABOT_AVATAR = 'https://avatars.githubusercontent.com/u/27347476?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' @@ -84,10 +82,14 @@ type CommentCardProps = { /** 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 muted circle instead of a - * photo `Avatar` (used for Copilot β€” audit "Copilot = octicon avatar"). + * 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 @@ -121,6 +123,8 @@ const CommentCard = ({ avatarSrc, avatarShape = 'circle', avatarIcon: AvatarIcon, + avatarIconShape = 'circle', + avatarIconTone = 'muted', associationLabel, associationAriaLabel, badgeLabel, @@ -135,7 +139,13 @@ const CommentCard = ({ {!isReply && ( {AvatarIcon ? ( - + ) : ( @@ -295,12 +305,12 @@ export const EventComment = () => ( Date: Sun, 28 Jun 2026 15:28:26 -0700 Subject: [PATCH 6/7] Tune comment rail/avatar/card geometry and drop reactions divider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match Figma: avatar left edge ~72px left of the rail, card overlaps the rail by 16px (so the rail runs behind the card's left edge), avatarβ†’card ~56px, with the caret bridging the gap. Remove the divider above the reactions row to match the live comment card. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...eline.comments.features.stories.module.css | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/packages/react/src/Timeline/Timeline.comments.features.stories.module.css b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css index a5772b85349..cde52fe2bc5 100644 --- a/packages/react/src/Timeline/Timeline.comments.features.stories.module.css +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css @@ -1,22 +1,18 @@ .RealisticTimeline { - /* GitHub renders the timeline at most 1012px wide in product surfaces. Just a - small left pad so the 40px gutter avatar (which sits ON the rail, see - .GutterAvatar) isn't clipped at the page edge β€” NOT a full avatar-column - reservation (the avatar overlays the gutter; the card is pushed right of it - via .Card's margin-left, so the two don't double-pad). */ + /* 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: var(--base-size-24); + padding-left: calc(var(--base-size-40) + var(--base-size-32)); } -/* Re-seat the 40px gutter avatar so it is centered ON the Timeline rail (the - Timeline.Item ::before line), rather than pushed far into the negative gutter - like the badge-row avatars. This makes the rail pass BEHIND the avatar and lets - the speech-bubble caret bridge avatar β†’ card. The default Timeline.Avatar `top` - / translateY (vertical centering) is preserved; only `left` is overridden. +/* 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-20)); + left: calc(-1 * (var(--base-size-40) + var(--base-size-32))); } /* Story-only scaffolding: each variant is wrapped in its own
    with a @@ -45,7 +41,7 @@ position: relative; flex: auto; min-width: 0; - margin-left: var(--base-size-32); + 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); @@ -161,13 +157,12 @@ border-radius: var(--borderRadius-medium); } -/* Reactions row below the body, separated by a divider β€” mirrors the reaction - bar GitHub renders under a comment. Each reaction is a small pill. */ +/* 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: var(--base-size-8) var(--base-size-16); - border-top: var(--borderWidth-thin) solid var(--borderColor-muted); + padding: 0 var(--base-size-16) var(--base-size-16); } .Reaction { From efcd2e55fc81dc6316894d6c4260aebd3a5ca504 Mon Sep 17 00:00:00 2001 From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com> Date: Sun, 28 Jun 2026 15:30:06 -0700 Subject: [PATCH 7/7] Guard onClick target with instanceof Element before closest() Avoid a runtime throw when the click target is a non-Element (e.g. an SVG octicon) by checking e.target instanceof Element before calling closest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../react/src/Timeline/Timeline.comments.features.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/Timeline/Timeline.comments.features.stories.tsx b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx index f53569694a4..9a382b8388f 100644 --- a/packages/react/src/Timeline/Timeline.comments.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx @@ -233,7 +233,7 @@ export const EventComment = () => ( className={classes.RealisticTimeline} // Prevent the placeholder `href="#"` links from navigating inside Storybook. onClick={e => { - if ((e.target as HTMLElement).closest('a')) e.preventDefault() + if (e.target instanceof Element && e.target.closest('a')) e.preventDefault() }} > {/* Standard USER comment: circular photo avatar, the "Author" subject-author badge