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..bc3a713b3fe --- /dev/null +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.module.css @@ -0,0 +1,9 @@ +/* Establishes the container query context for the responsive comment collapse. The + large comment cards switch between the large-gutter form (≥768px) and the compact + inline form (<768px) based on THIS element's inline size — mirroring live + github.com, which uses a container query (`@container issueViewerContainer`), not a + viewport media query, at a 768px breakpoint. The card + its `@container` child rules + live in `internal/CommentCard.module.css`; the story just owns the container root. */ +.CommentsRoot { + container-type: inline-size; +} 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..f04f258bb3f --- /dev/null +++ b/packages/react/src/Timeline/Timeline.comments.features.stories.tsx @@ -0,0 +1,245 @@ +import type {Meta} from '@storybook/react-vite' +import type React from 'react' +import type {ComponentProps} from '../utils/types' +import {FeatureFlags} from '../FeatureFlags' +import Timeline from './Timeline' +import {CopilotIcon, DependabotIcon} from '@primer/octicons-react' +import Link from '../Link' +import {RealisticTimeline, VariantSection} from './internal/timelineStoryHelpers' +import {CommentCard} from './internal/CommentCard' +import cardClasses from './internal/CommentCard.module.css' +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`). + * + * The reusable card composition lives in `./internal/CommentCard` (extracted so other + * event surfaces can reuse it, per the #8072 review); this file only holds the example + * data + variant catalog. See `CommentCard.tsx` for the avatar matrix (user/bot/Copilot/ + * Dependabot), the parent-child app-avatar reconstruction, the responsive collapse, and + * the verified live denotations. + * + * TITLE / IA: The story is a LEAF named "Comment cards" directly under + * `Components/Timeline/Internal` (title `Components/Timeline/Internal` + export + * `CommentCards`), a sibling of the "Helpers" leaf. Storybook sidebar location is driven + * by the `title` string + story name, decoupled from code location. There is only ever + * one comment story and it documents the internal `CommentCard` component, so it lives + * under `Internal/` next to the helper docs (keeping the tree small). + * + * SCOPE: Storybook-only by design, like the badge-row event stories. 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. + */ + +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' + +/** + * Story-only scaffolding: the shared captioned `VariantSection` wrapping a single + * ``, so each card renders as it would in product. + */ +const CommentSection = ({label, children}: {label: string; children: React.ReactNode}) => ( + + {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 CommentCards = () => ( + +
{ + if (e.target instanceof Element && e.target.closest('a')) e.preventDefault() + }} + > + {/* Default (large-gutter) comment forms: a 40px avatar sits in the left gutter and + the card is indented past it. The `.GutterGroup` reserves that ~72px column + (from CommentCard's module, since the responsive `@container` rules toggle it). */} +
+ {/* 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`. */} + + +

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

+
+
+ + {/* User comment via an app, WITH the app's avatar badge overlapping the author avatar + (the parent-child avatar pattern — GitHub renders this for app/agent co-authored + comments, e.g. "monalisa … – with GitHub Actions"). The small app avatar (rounded + square) sits at the bottom-right of the large author avatar. We reconstruct it + with a symmetric offset + opaque ring, avoiding Primer `.avatar-child`'s + long-standing asymmetric offset (github/github#439417). */} + + +

Deploy preview is ready — the changes are live on the staging environment.

+
+
+
+ + {/* Inline-avatar (compact) comment: NO left gutter / 40px avatar column — a ~24px + avatar sits inline in the header before the author, and the body spans full + width. This is BOTH the new Issues `issue_inline_avatars` rendering + (github-ui `ActivityHeader` forceInlineAvatar branch) AND the responsive + narrow-viewport form for all comments. Rendered as a full-width sibling of the + `.GutterGroup` above so it gets no gutter indent. */} + + +

+ Same comment, compact form — the avatar is inline in the header and the body runs full width with no gutter. +

+
+
+
+
+) + +// Force the exact sentence-case leaf label (Storybook would otherwise title-case the +// export `CommentCards` to "Comment Cards"). Story ID stays `--comment-cards`. +CommentCards.storyName = 'Comment cards' + +export default { + title: 'Components/Timeline/Internal', + 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> diff --git a/packages/react/src/Timeline/internal/CommentCard.module.css b/packages/react/src/Timeline/internal/CommentCard.module.css new file mode 100644 index 00000000000..9f99868deb8 --- /dev/null +++ b/packages/react/src/Timeline/internal/CommentCard.module.css @@ -0,0 +1,347 @@ +/* Reserve the ~72px gutter for the large-avatar comment forms so the 40px avatar + (which sits left of the rail) isn't clipped. Only applies at ≥768px; below that the + cards collapse to the gutter-less inline form (live `.nonLeadingElement` only gets + its 56px indent inside `@container (min-width: 768px)`). The container itself + (`container-type: inline-size`) is established by the consuming story on a root + wrapper, so this responsive block resolves against the timeline width. */ +.GutterGroup { + padding-left: 0; +} + +@container (min-width: 768px) { + .GutterGroup { + 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 (.GutterGroup .GutterAvatar = 0-2-0) beats the base .TimelineItemAvatar + (0-1-0) regardless of stylesheet order. The gutter avatar is HIDDEN below 768px + (the inline header avatar shows instead — live `.avatarHiddenOnMedium`). */ +.GutterGroup .GutterAvatar { + left: calc(-1 * (var(--base-size-40) + var(--base-size-32))); + display: none; +} + +@container (min-width: 768px) { + .GutterGroup .GutterAvatar { + display: block; + } +} + +/* Parent-child avatar: a small app/agent avatar overlapping the bottom-right of the + large author avatar (GitHub renders this for app/agent co-authored comments). Based + on Primer's `.avatar-parent-child` / `.avatar-child`, but deliberately CORRECTED: + the upstream `.avatar-child` uses an asymmetric `right: -15%` / `bottom: -9%` offset + and a translucent `#fffc` box-shadow, which makes the badge sit unevenly with a + lopsided ring (github/github#439417). We use a SYMMETRIC offset and an OPAQUE ring so + the badge sits cleanly in the corner. */ +.AvatarParent { + position: relative; + display: inline-flex; +} + +.AppAvatarChild { + position: absolute; + right: calc(-1 * var(--base-size-4)); + bottom: calc(-1 * var(--base-size-4)); + width: var(--base-size-20); + height: var(--base-size-20); + /* App avatars are rounded squares (vs circular user avatars). */ + border-radius: var(--borderRadius-medium); + /* Opaque canvas ring separating the child from the parent (not the translucent + upstream shadow). Mirrors Primer's own `.avatar-child` ring, which is likewise a + custom box-shadow (`--avatar-shadow`), so no token maps to it. */ + /* stylelint-disable-next-line primer/box-shadow -- avatar separator ring, matches upstream .avatar-child */ + box-shadow: 0 0 0 var(--borderWidth-thick) var(--bgColor-default); +} + +/* 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); +} + +/* Inline header avatar (~24px) that mirrors the actor in the compact / collapsed + form. Photo actors use the shared InlineAvatar; octicon actors (Copilot, + Dependabot) use `.InlineOcticonAvatar` — a 24px inline version of the octicon + avatar, composing the shared shape/tone modifiers below. */ +.InlineOcticonAvatar { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--base-size-24); + height: var(--base-size-24); + margin-right: var(--base-size-4); + color: var(--fgColor-muted); + background-color: var(--bgColor-muted); + border: var(--borderWidth-thin) solid var(--borderColor-default); + border-radius: var(--borderRadius-full); + vertical-align: middle; +} + +/* 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; +} + +/* Inline-avatar (compact) card: no left-gutter avatar to point at, so drop the + speech-bubble caret. It still OVERLAPS the rail by ~16px (same as the large `.Card`) + so the rail sits behind the card's opaque left edge — matching live github.com. */ +.CardInline { + margin-left: calc(-1 * var(--base-size-16)); +} + +.CardInline::before, +.CardInline::after { + display: none; +} + +/* Center the header row so the inline ~24px avatar aligns with the author/text + (the default baseline alignment is for the text-only large-gutter header). */ +.CardInline .HeaderText { + align-items: center; +} + +/* Inline header avatar hook (photo InlineAvatar or InlineOcticonAvatar): don't let it + shrink, and it's the toggle target for the responsive collapse. */ +.InlineHeaderAvatar { + flex-shrink: 0; +} + +/* Muted "commented" verb in the inline/compact header ("{author} commented {time}"). */ +.CommentedVerb { + color: var(--fgColor-muted); +} + +/* Responsive collapse for the LARGE cards: below 768px they render the compact inline + form (inline avatar + "commented" verb, centered header); at ≥768px they use the + large gutter avatar and the inline header parts are hidden (live + `.avatarHiddenOnMedium`). The always-inline `.CardInline` demo is unaffected. */ +.CardResponsive .HeaderText { + align-items: center; +} + +/* Below 768px the responsive card is collapsed (gutter avatar hidden, avatar inline), + so hide its speech-bubble caret — there's no gutter avatar for it to point at. These + rules come AFTER `.Card::before`/`::after` (equal specificity) so they win in the base + state; the caret is restored at ≥768px in the container block below. */ +.CardResponsive::before, +.CardResponsive::after { + display: none; +} + +@container (min-width: 768px) { + .CardResponsive .InlineHeaderAvatar, + .CardResponsive .CommentedVerb { + display: none; + } + + .CardResponsive .HeaderText { + align-items: baseline; + } + + /* Restore the caret at ≥768px, where the large gutter avatar is shown and the caret + bridges avatar → card. */ + .CardResponsive::before, + .CardResponsive::after { + display: block; + } +} diff --git a/packages/react/src/Timeline/internal/CommentCard.tsx b/packages/react/src/Timeline/internal/CommentCard.tsx new file mode 100644 index 00000000000..5f1f7e08837 --- /dev/null +++ b/packages/react/src/Timeline/internal/CommentCard.tsx @@ -0,0 +1,245 @@ +import type React from 'react' +import {clsx} from 'clsx' +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 Timeline from '../Timeline' +import {InlineAvatar} from './timelineStoryHelpers' +import classes from './CommentCard.module.css' + +/** + * Internal, reusable comment-card composition for the Timeline story surfaces. NOT part + * of the public API (stories don't ship; not exported from the package index — same as + * `timelineStoryHelpers`). Extracted from the Comments story so it can be reused across + * event-story surfaces rather than baked into a single story file (#8072 review). + * + * There is no Primer "Comment" primitive, so the card is composed directly from Primer + * primitives + a scoped CSS module: a `Timeline.Item` with a 40px gutter avatar seated + * on the rail, and a bordered card (header bar with author + badges + timestamp + + * actions, markdown body, optional reactions, speech-bubble caret bridging avatar → + * card). + * + * RESPONSIVE: the default large-gutter card COLLAPSES to the compact inline form below + * 768px (both avatars stay in the DOM, toggled by a container query — mirrors live + * github.com: gutter avatar + 56px indent only at ≥768px; small inline header avatar + + * full-width body below). The consuming story establishes the container + * (`container-type: inline-size`) on a root wrapper and uses `commentCardClasses.GutterGroup` + * to wrap the large-gutter sections. + * + * AVATAR MATRIX (verified against github-ui `ActivityHeader` + labels): users = circle + * photo; bots (github-actions) = square photo; Copilot = muted `CopilotIcon` octicon in + * a circle; Dependabot = white `DependabotIcon` on an accent-blue rounded square (Figma + * brand spec). App/agent co-authored comments add a small app avatar overlapping the + * bottom-right of the author avatar (parent-child), reconstructed with a symmetric + * offset + opaque ring to avoid Primer `.avatar-child`'s asymmetric offset + * (github/github#439417). + */ + +export 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} + /** + * A small app/agent avatar overlapping the bottom-right of the large gutter avatar + * (the "parent-child" avatar pattern GitHub renders for app/agent co-authored + * comments, e.g. "monalisa … – with Claude"). App avatars are rounded squares. + */ + appAvatar?: {src: string; alt: string} + /** Show the reactions footer. */ + reactions?: boolean + /** + * ALWAYS-inline (compact) form: no 40px left-gutter avatar at any width; instead a + * ~24px avatar sits inline in the header before the author, the header reads + * "{author} commented {time}", and the body spans full width with no gutter indent or + * speech-bubble caret. This is the new Issues `issue_inline_avatars` rendering + * (github-ui `ActivityHeader` `forceInlineAvatar` branch). + * + * NOTE: the DEFAULT (large-gutter) cards ALSO collapse to this exact form below 768px + * responsively (via the container query in `CommentCard.module.css`) — that is the + * live narrow-viewport behavior (`.avatarHiddenOnMedium` + `.nonLeadingElement`'s 56px + * indent only inside `@container (min-width: 768px)`). This prop forces the compact + * form at all widths for the dedicated demo section. + */ + inlineAvatar?: 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 +} + +export const CommentCard = ({ + authorName, + authorHref = '#', + avatarSrc, + avatarShape = 'circle', + avatarIcon: AvatarIcon, + avatarIconShape = 'circle', + avatarIconTone = 'muted', + associationLabel, + associationAriaLabel, + badgeLabel, + badgeAriaLabel, + timestamp, + viaApp, + appAvatar, + reactions = false, + inlineAvatar = false, + isReply = false, + children, +}: CommentCardProps) => { + // A large-gutter card that responsively COLLAPSES to the inline form below 768px + // (both avatars are in the DOM; CSS toggles them via a container query, mirroring + // live github-ui). The always-inline compact demo (`inlineAvatar`) and threaded + // replies (`isReply`) are not responsive. + const isResponsive = !isReply && !inlineAvatar + // Whether an inline header avatar + "commented" verb is present: for the + // always-inline demo (shown at all widths) and for responsive cards (shown only + // <768px via CSS). Threaded replies have no avatar. + const hasInlineHeader = !isReply + + // The 40px gutter avatar (octicon actor, or photo, optionally with an app badge). + const gutterAvatarNode = AvatarIcon ? ( + + + + ) : ( + + + {appAvatar ? ( + // Parent-child avatar: the small app avatar overlaps the bottom-right of + // the large avatar. Primer's `.avatar-child` has a long-standing asymmetric + // offset (right: -15% / bottom: -9%) and a translucent white shadow; we + // deliberately reconstruct it with a SYMMETRIC offset and an opaque ring so + // the badge sits cleanly in the corner (github/github#439417). + {appAvatar.alt} + ) : null} + + ) + + // The ~24px inline header avatar mirroring the actor (octicon actors keep their + // octicon; everyone else uses the shared 24px photo InlineAvatar). + const inlineAvatarNode = AvatarIcon ? ( + + + + ) : ( + + ) + + return ( + + {!isReply && !inlineAvatar && ( + {gutterAvatarNode} + )} +
    +
    +
    + {/* Inline-avatar form: a ~24px avatar sits inline in the header before the + author (github-ui `ActivityHeader` forceInlineAvatar / DefaultAvatar + size={24}). Shown always for the compact demo, and only <768px for the + responsive large cards (CSS container query). */} + {hasInlineHeader ? inlineAvatarNode : null} + {/* 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} + {/* The inline/compact header reads "{author} commented {time}", matching the + live forceInlineAvatar header phrasing. */} + {hasInlineHeader ? commented : 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} +
    +
    + ) +} diff --git a/packages/react/src/Timeline/internal/timelineStoryHelpers.module.css b/packages/react/src/Timeline/internal/timelineStoryHelpers.module.css new file mode 100644 index 00000000000..9d1e3f82156 --- /dev/null +++ b/packages/react/src/Timeline/internal/timelineStoryHelpers.module.css @@ -0,0 +1,39 @@ +.RealisticTimeline { + /* GitHub renders the timeline at most 1012px wide in product surfaces. */ + max-width: 1012px; +} + +.VariantSection { + margin-bottom: var(--base-size-24); +} + +.VariantLabel { + margin: 0 0 var(--base-size-4); + 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; +} + +/* TODO(github/primer#6826): remove when Primer Link ships a bold/actor weight affordance */ +.BoldLink { + font-weight: var(--base-text-weight-semibold); + color: var(--fgColor-default); + margin-right: var(--base-size-4); +} + +.BoldLink:hover { + color: var(--fgColor-accent); +} + +/* TODO(github/primer#6827): remove when Primer ships an inline (in-text) avatar treatment */ +.InlineAvatar { + vertical-align: middle; + margin-right: var(--base-size-4); +} + +/* TODO(github/primer#6828): remove when Timeline provides a muted relative-time treatment */ +.MutedTime { + color: var(--fgColor-muted); +} diff --git a/packages/react/src/Timeline/internal/timelineStoryHelpers.stories.tsx b/packages/react/src/Timeline/internal/timelineStoryHelpers.stories.tsx new file mode 100644 index 00000000000..cb08804ffe6 --- /dev/null +++ b/packages/react/src/Timeline/internal/timelineStoryHelpers.stories.tsx @@ -0,0 +1,62 @@ +import type {Meta} from '@storybook/react-vite' +import {BoldLink, Examples, InlineAvatar, MutedTime, RealisticTimeline, VariantSection} from './timelineStoryHelpers' + +/** + * Reference stories for the internal, story-only Timeline helpers + * (`./timelineStoryHelpers`). These are NOT public components — they're shared + * scaffolding for the Timeline event-story surfaces, rendered here in isolation so the + * module is discoverable and self-documenting. + * + * `BoldLink`, `InlineAvatar`, and `MutedTime` reproduce live-GitHub styling that Primer + * doesn't express natively yet — tracked to remove in + * github/primer#6826 / #6827 / #6828. + */ + +const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4' + +export const Helpers = () => ( + + +

    Wraps a story variant with an uppercase muted caption heading. Used for every label on this page.

    +
    + + +

    Bare container constraining content to GitHub's product timeline width. (This page is inside one.)

    +
    + + + +

    + Wraps surfaces that render in-text links: adds the data-a11y-link-underlines hint and a click + guard so placeholder href="#" links don't navigate inside Storybook. +

    +
    +
    + + +

    + monalisa opened this — the actor link renders bold until Primer ships a bold/actor + weight affordance. +

    +
    + + +

    + a 20px inline avatar (override size for 24px), pending an + upstream inline avatar treatment. +

    +
    + + +

    + Updated — muted relative time, pending a Timeline muted + time treatment. +

    +
    +
    +) + +export default { + title: 'Components/Timeline/Internal', + component: VariantSection, +} as Meta diff --git a/packages/react/src/Timeline/internal/timelineStoryHelpers.tsx b/packages/react/src/Timeline/internal/timelineStoryHelpers.tsx new file mode 100644 index 00000000000..0097949f625 --- /dev/null +++ b/packages/react/src/Timeline/internal/timelineStoryHelpers.tsx @@ -0,0 +1,70 @@ +import type React from 'react' +import {clsx} from 'clsx' +import Avatar from '../../Avatar' +import Link from '../../Link' +import RelativeTime from '../../RelativeTime' +import classes from './timelineStoryHelpers.module.css' + +/** + * Internal, story-only Timeline helpers. Not part of the public API (stories don't + * ship). The BoldLink/InlineAvatar/MutedTime helpers reproduce live-GitHub styling + * Primer doesn't express natively yet — tracked to remove in + * github/primer#6826/#6827/#6828. + * + * Shared across the Timeline event-story surfaces (Issues, Comments, and the other + * per-surface story files) so the scaffolding stays consistent and the live-GitHub + * shims live in exactly one place. + */ + +/** + * A captioned `
    ` wrapping one story variant: an uppercase muted caption + * heading above the variant content. Consumers pass the timeline (or card) as + * `children`. + */ +export const VariantSection = ({label, children}: {label: string; children: React.ReactNode}) => ( +
    +

    {label}

    + {children} +
    +) + +/** + * Bare max-width container matching GitHub's product timeline width (1012px). Used by + * surfaces that have no in-text links (and therefore need no click guard). Surfaces + * with in-text placeholder links should use `Examples` instead. + */ +export const RealisticTimeline = ({children}: {children: React.ReactNode}) => ( +
    {children}
    +) + +/** + * `RealisticTimeline` plus the story-only affordances for surfaces that render in-text + * links: the `data-a11y-link-underlines` hint and a click guard that prevents the + * placeholder `href="#"` links from navigating inside Storybook. + */ +export const Examples = ({children}: {children: React.ReactNode}) => ( +
    { + if (e.target instanceof Element && e.target.closest('a')) e.preventDefault() + }} + > + {children} +
    +) + +// TODO(github/primer#6826): remove when Primer Link ships a bold/actor weight affordance +export const BoldLink = ({className, ...props}: React.ComponentProps) => ( + +) + +// TODO(github/primer#6827): remove when Primer ships an inline (in-text) avatar treatment +export const InlineAvatar = ({className, size = 20, alt = '', ...props}: React.ComponentProps) => ( + +) + +// TODO(github/primer#6828): remove when Timeline provides a muted relative-time treatment +export const MutedTime = ({date}: {date: Date}) => ( + +)