diff --git a/packages/react/src/Timeline/Timeline.dependabot.features.stories.module.css b/packages/react/src/Timeline/Timeline.dependabot.features.stories.module.css new file mode 100644 index 00000000000..c6c90b1477b --- /dev/null +++ b/packages/react/src/Timeline/Timeline.dependabot.features.stories.module.css @@ -0,0 +1,127 @@ +.RealisticTimeline { + /* GitHub renders the timeline at most 1012px wide in product surfaces. */ + max-width: 1012px; +} + +/* Story-only scaffolding: each variant is wrapped in its own
with a + small caption heading ABOVE the event, so the event row itself (badge + + inline avatar + body) renders exactly as it would in product — the caption + never sits inside Timeline.Body. Variants stay scannable like a Figma + component set. Not part of the faithful event. */ +.Variant { + 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; +} + +/* Inline 20px-ish actor avatar in the body. The Dependabot actor renders a + SQUARE avatar (live ERB `ActorComponent` uses `GitHub::AvatarComponent` with + `is_user: false` → square, the bundled `dependabot-icon.png`). */ +.InlineAvatar { + vertical-align: middle; + margin-right: var(--base-size-4); +} + +/* Bold actor / reference link: live ERB renders these via + `Primer::Beta::Link.new(scheme: :primary, font_weight: :bold)` — semibold, + default foreground, accent on hover. Used for the bold `dependabot` actor + link and the bold `#123` pull-request link. */ +.LinkWithBoldStyle { + font-weight: var(--base-text-weight-semibold); + color: var(--fgColor-default); + margin-right: var(--base-size-4); +} + +.LinkWithBoldStyle:hover { + color: var(--fgColor-accent); +} + +/* `(bot)` identifier tag. Live ERB `bot_identifier_tag` renders + `bot`; mirrored here with Primer + `Label variant="secondary"`. This class only adds inline spacing. */ +.BotLabel { + margin: 0 var(--base-size-4) 0 0; + vertical-align: middle; +} + +/* Muted relative timestamp. Unlike the Issues timeline (whose `Ago` renders a + muted UNDERLINED deep-link), the Dependabot ERB renders a plain + `Primer::Beta::RelativeTime` with no link wrapper — muted text only. */ +.Timestamp { + color: var(--fgColor-muted); +} + +/* Push-pill: the `(push-pill: SHA)` SUBTLE light-blue rounded pill from + `PushLinkComponent` (`` wrapping `Primer::Beta::Link` with + `bg: :accent, px: 2, py: 1, border_radius: 3`). In primer/view_components + `bg: :accent` resolves to `.color-bg-accent` = the SUBTLE/MUTED light-blue + background (`--color-accent-subtle`), NOT the solid emphasis blue — so the + text stays accent-blue (the Link's default color), not white. This is the + standard GitHub SHA push-pill look. Primer `Link`'s own color rule is + `:where(.Link)` (zero specificity), so this CSS-module class fully drives the + pill appearance. */ +.PushPill { + /* The wrapping element — no own box, the inner link is the pill. */ + font-family: var(--fontStack-monospace); +} + +.PushPillLink { + display: inline-block; + font-family: var(--fontStack-monospace); + font-size: var(--text-body-size-small); + color: var(--fgColor-accent); + background-color: var(--bgColor-accent-muted); + padding: var(--base-size-2) var(--base-size-4); + border-radius: var(--borderRadius-medium); + text-decoration: none; +} + +.PushPillLink:hover { + text-decoration: none; +} + +/* Optional dismissal/auto sub-content row. Live ERB renders a + `
` (small, indented block + below the event) holding a `note` octicon + the comment text. */ +.NoteComment { + margin-top: var(--base-size-8); + padding-left: var(--base-size-4); + font-size: var(--text-body-size-small); + color: var(--fgColor-muted); +} + +.NoteIcon { + vertical-align: middle; + margin-right: var(--base-size-4); + color: var(--fgColor-muted); +} + +/* Colored badge icons for the Dismissal Request / Review / Cancelled events. + Their live ERB renders `with_badge(color: :X, icon: …)` with NO `bg:` — in + Primer `color:` tints the ICON on the DEFAULT badge background (it is NOT a + solid-color badge). So these render as a bare `` (default gray + circle) with the icon color set here, mirroring the Dismissed muted icon. */ +.BadgeIconAttention { + color: var(--fgColor-attention); +} + +.BadgeIconSuccess { + color: var(--fgColor-success); +} + +.BadgeIconDanger { + color: var(--fgColor-danger); +} + +/* `:subtle` icon color. Primer has no `--fgColor-subtle` token, so the closest + functional equivalent for the Dismissal Cancelled `color: :subtle` is muted. */ +.BadgeIconMuted { + color: var(--fgColor-muted); +} diff --git a/packages/react/src/Timeline/Timeline.dependabot.features.stories.tsx b/packages/react/src/Timeline/Timeline.dependabot.features.stories.tsx new file mode 100644 index 00000000000..f0e966391d0 --- /dev/null +++ b/packages/react/src/Timeline/Timeline.dependabot.features.stories.tsx @@ -0,0 +1,715 @@ +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 { + CheckIcon, + CommentIcon, + NoteIcon, + ShieldCheckIcon, + ShieldIcon, + ShieldSlashIcon, + SyncIcon, + XIcon, +} from '@primer/octicons-react' +import Avatar from '../Avatar' +import {Button} from '../Button' +import Label from '../Label' +import Link from '../Link' +import Octicon from '../Octicon' +import RelativeTime from '../RelativeTime' +import classes from './Timeline.dependabot.features.stories.module.css' + +/** + * Dependabot alert Timeline event examples (Phase 2 of github/primer#6663). + * + * These stories recreate GitHub's live Dependabot-alert-timeline events using + * the Primer `Timeline` compositional slots. They mirror the established Issues + * stories (`Timeline.issues.features.stories.tsx`) and are sourced from the + * `timeline-audit` Figma audit (`dependabot-timeline-events-for-figma.md`). + * + * SOURCE OF TRUTH — Dependabot is NOT (yet) migrated to React. The Dependabot + * alert timeline is entirely SERVER-RENDERED ERB (ViewComponent). The events + * are rendered by `DependabotAlerts::TimelineComponent`, which dispatches to + * per-event components in `app/components/dependabot_alerts/timeline_items/` + * (e.g. `OpenedComponent`) in the `github/github` Rails monorepo. There is no + * React equivalent in `github/github-ui` — that repo only ships Catalyst + * custom-element controllers (`dependabot-alert-*-element.ts`) for table-row / + * load-all / dismissal interactions, not the timeline event rows. So each event + * below is translated faithfully from the live ERB into Primer React, with the + * ERB source file commented inline. + * + * SCOPE: These are Storybook-only examples by design. 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 — the primer.style Timeline page reflects the + * base `Timeline` component's own stories, and any docs-site representation is a + * Phase 3 consideration via base-component story changes, out of scope here. + * + * FUTURE FILTERING (taxonomy still open — github/primer#6663): category + * `data-*` attributes (e.g. `data-event-category="opened"`) will attach to each + * `Timeline.Item` below so stories can be filtered/grouped by event family. We + * intentionally do NOT add them yet to avoid baking in a taxonomy. + * + * SLOT USAGE (Phase 1 slots — same convention as the Issues group): + * - `Timeline.Avatar` (gutter slot, #6677): the 40px LEFT-GUTTER avatar. + * Reserved for comment-style events. Badge-row events like Opened do NOT use + * it — the live ERB renders the actor's small SQUARE avatar INLINE in the body + * (`ActorComponent`), not in the gutter. We mirror that: avatar inline in + * `Timeline.Body`. + * - `Timeline.Actions` (right-controls slot, #6678): for buttons / SHAs / status + * pills on the right edge. Opened has no right controls, so it is omitted here. + * + * DEPENDABOT-SPECIFIC COMPOSITION (see helpers below): the square bot avatar, + * the `(bot)` identifier tag, and the SUBTLE light-blue `(push-pill: SHA)` are + * composed from Primer primitives (`Avatar square`, `Label variant="secondary"`, + * and a ``-wrapped styled `Link`) to match the live ERB. + */ + +// Live ERB uses the bundled `modules/site/dependabot-icon.png`; this is the +// public dependabot[bot] avatar (square Dependabot logo) for Storybook. +const DEPENDABOT_AVATAR = 'https://avatars.githubusercontent.com/u/27347476?v=4' +const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4' + +/** + * Dependabot actor — `DependabotAlerts::TimelineItems::ActorComponent` in + * `:dependabot` mode (`actor_component.html.erb`): + * square avatar + bold `dependabot` docs link + `(bot)` identifier tag. + * (`bot_identifier_tag` → `bot`.) + * Used by Dependabot-authored events (Opened / Fixed / Reintroduced / + * Auto-dismissed / Auto-reopened). + */ +const DependabotActor = () => ( + <> + + + dependabot + + + +) + +/** + * User actor — `ActorComponent` regular-user branch (`linked_avatar_for(actor, + * 20)` + `profile_link(... "text-bold")`): a CIRCLE 20px avatar + bold profile + * link, no `(bot)` tag. Used by user-authored events (manual Dismissed, manual + * Reopened, Dismissal Cancelled). + */ +const UserActor = () => ( + <> + + + monalisa + + +) + +// Muted relative timestamp. The Dependabot ERB renders a plain +// `Primer::Beta::RelativeTime` (no link wrapper) — muted text only, which is a +// deliberate difference from the Issues `Ago` deep-link. +const Time = ({date}: {date: string}) => ( + + + +) + +/** + * Push-pill — `PushLinkComponent`: a `` wrapping a `Primer::Beta::Link` + * (`bg: :accent, px: 2, py: 1, border_radius: 3`) → SUBTLE light-blue rounded + * pill with accent-blue monospace text (the standard GitHub SHA pill), showing + * the 7-char `after` SHA (single commit) or a `before..after` range (multi). + */ +const PushPill = ({sha}: {sha: string}) => ( + + + {sha} + + +) + +/** + * Optional sub-content row rendered below a dismissal/auto event — the live ERB + * renders a `
` with a `note` + * octicon and the comment text. Rendered here as a small muted block inside the + * event body. + */ +const NoteComment = ({children}: {children: React.ReactNode}) => ( +
+ + {children} +
+) + +export default { + title: 'Components/Timeline/Events/Dependabot', + component: Timeline, + subcomponents: { + 'Timeline.Item': Timeline.Item, + 'Timeline.Avatar': Timeline.Avatar, + 'Timeline.Badge': Timeline.Badge, + 'Timeline.Body': Timeline.Body, + 'Timeline.Break': Timeline.Break, + 'Timeline.Actions': Timeline.Actions, + }, + 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> + +/** + * The Opened event group — `DepTimeline.eventOpened` (audit § 1). + * + * Source: `OpenedComponent` (`opened_component.html.erb`). The actor is ALWAYS + * Dependabot. Badge: `shield` icon on `success` (green) — the ERB renders + * `with_badge(bg: :success_emphasis, color: :on_emphasis, icon: :shield)`, + * which maps exactly to `Timeline.Badge variant="success"`. + * + * Three source variants (`Opened` / `OpenedFromPR` / `OpenedFromPush`) differ + * only by the optional "from …" clause: no source, a bold `#123` PR link, or a + * blue push-pill. + */ +export const EventOpened = () => ( +
    { + if ((e.target as HTMLElement).closest('a')) e.preventDefault() + }} + > + {/* Opened — no source */} +
    +

    Opened

    + + + + + + + + {'opened this '} + + + +
    + + {/* OpenedFromPR — bold `#123` pull-request link (scheme: primary, bold) */} +
    +

    Opened from pull request

    + + + + + + + + {'opened this from '} + + #123 + {' '} + + + +
    + + {/* OpenedFromPush — blue push-pill with the 7-char `after` SHA */} +
    +

    Opened from push

    + + + + + + + + {'opened this from '} + + + +
    +
    +) + +/** + * The Fixed event group — `DepTimeline.eventFixed` (audit § 2). + * + * Source: `FixedComponent` (`fixed_component.html.erb`). Actor is ALWAYS + * Dependabot. Badge: `shield-check` on `done` (purple) — the ERB renders + * `with_badge(bg: :done_emphasis, color: :on_emphasis, icon: "shield-check")`, + * mapping to `Timeline.Badge variant="done"`. Three source variants differ only + * by the optional "in …" clause (no source / bold `#123` PR link / push-pill). + * + * NOTE: the live ERB renders a `TimelineItem-break` separator after this event + * (unless it is the last). In these stacked, single-event stories we omit the + * runtime separator — it is a list-position concern, not part of the event. + */ +export const EventFixed = () => ( +
    { + if ((e.target as HTMLElement).closest('a')) e.preventDefault() + }} + > + {/* Fixed — no source */} +
    +

    Fixed

    + + + + + + + + {'closed this as completed '} + + + +
    + + {/* FixedViaPR — bold `#123` pull-request link */} +
    +

    Fixed via pull request

    + + + + + + + + {'closed this as completed in '} + + #123 + {' '} + + + +
    + + {/* FixedViaPush — blue push-pill */} +
    +

    Fixed via push

    + + + + + + + + {'closed this as completed in '} + + + +
    +
    +) + +/** + * The Dismissed event group — `DepTimeline.eventDismissed` (audit § 3). + * + * Consolidates MANUAL user dismissals (`DismissedComponent`, circle user actor) + * and rule-based AUTO dismissals (`AutoDismissedComponent`, square Dependabot + * actor). Badge: `shield-slash` on the PLAIN DEFAULT badge (subtle gray circle, + * muted icon) — the live ERB renders `with_badge(color: :muted, ...)` / + * `with_badge(icon: "shield-slash")` with NO `bg:` override, so we use a bare + * `Timeline.Badge` (no variant), NOT the neutral-emphasis hook. + * + * Manual reasons are `RepositoryVulnerabilityAlert::DISMISS_REASONS` downcased. + * The optional comment renders as a small indented sub-row (note octicon + text). + */ +export const EventDismissed = () => ( +
    { + if ((e.target as HTMLElement).closest('a')) e.preventDefault() + }} + > + {/* Manual — risk is tolerable (with an optional dismissal note) */} +
    +

    Dismissed as risk is tolerable

    + + + + + + + + {'dismissed this as risk is tolerable '} + + + +
    + + {/* Manual — fix started */} +
    +

    Dismissed as fix started

    + + + + + + + + {'dismissed this as fix started '} + + + +
    + + {/* Manual — no bandwidth to fix this */} +
    +

    Dismissed as no bandwidth

    + + + + + + + + {'dismissed this as no bandwidth to fix this '} + + + +
    + + {/* Manual — vulnerable code is not actually used */} +
    +

    Dismissed as not used

    + + + + + + + + {'dismissed this as vulnerable code is not actually used '} + + + +
    + + {/* Manual — inaccurate */} +
    +

    Dismissed as inaccurate

    + + + + + + + + {'dismissed this as inaccurate '} + + + +
    + + {/* Auto — rule-based, no source (with optional rule comment) */} +
    +

    Auto-dismissed by alert rule

    + + + + + + + + {'dismissed this due to an alert rule '} + + + +
    + + {/* Auto — from a pull request */} +
    +

    Auto-dismissed from pull request

    + + + + + + + + {'dismissed this due to an alert rule from '} + + #123 + {' '} + + + +
    + + {/* Auto — from a push */} +
    +

    Auto-dismissed from push

    + + + + + + + + {'dismissed this due to an alert rule from '} + + + +
    +
    +) + +/** + * The Reopened event group — `DepTimeline.eventReopened` (audit § 4). + * + * Consolidates MANUAL reopens (`ReopenedComponent`, circle user actor), + * REINTRODUCTIONS (`ReintroducedComponent`, square Dependabot actor, × source) + * and rule-based AUTO reopens (`AutoReopenedComponent`, square Dependabot + * actor). Badge: `sync` on `success` (green) for all — the ERB renders + * `with_badge(bg: :success_emphasis, color: :on_emphasis, icon: :sync)`. + */ +export const EventReopened = () => ( +
    { + if ((e.target as HTMLElement).closest('a')) e.preventDefault() + }} + > + {/* Manual reopen — user actor */} +
    +

    Reopened

    + + + + + + + + {'reopened this '} + + + +
    + + {/* Reintroduced — no source */} +
    +

    Reintroduced

    + + + + + + + + {'reopened this '} + + + +
    + + {/* Reintroduced — from a pull request */} +
    +

    Reintroduced from pull request

    + + + + + + + + {'reopened this from '} + + #123 + {' '} + + + +
    + + {/* Reintroduced — from a push */} +
    +

    Reintroduced from push

    + + + + + + + + {'reopened this from '} + + + +
    + + {/* Auto-reopened — rule change (with optional rule comment) */} +
    +

    Auto-reopened by rule change

    + + + + + + + + {'reopened this '} + + + +
    +
    +) + +/** + * The Dismissal Request event group — `DepTimeline.eventDismissalRequest` + * (audit § 5). Part of the org-level delegated-closures system. + * + * SOURCE OF TRUTH — inline `erb_template` (NOT the dormant sidecar): each + * `dismissal_*_component.rb` does `include ViewComponent::InlineTemplate` and + * defines an `erb_template`, which is the ACTIVE render; the sibling + * `*.html.erb` sidecar is dead code left in the tree. So every variant here + * renders the actor via `ActorComponent` (a USER → circle avatar + bold link, + * our `UserActor`), with status-driven badges: + * - Requested (`DismissalRequestedComponent`): `comment` icon, attention color. + * - Approved (`DismissalReviewedComponent`, status approved): `check`, success. + * - Denied (`DismissalReviewedComponent`, status rejected): `x`, danger. + * - Cancelled (`DismissalCancelledComponent`): `x`, subtle color. + * + * BADGE MECHANIC: the ERB uses `with_badge(color: :X, icon: Y)` with NO `bg:`. + * In Primer that tints the ICON on the DEFAULT badge background — it does NOT + * produce a solid-color badge (unlike Opened/Fixed/Reopened, which DO set + * `bg: :X_emphasis` and so use solid `variant`s). So these four render as a bare + * `` (default gray circle) with the icon color set via a + * `.BadgeIcon*` class — the same structure as the muted Dismissed badge. + * Optional review/resolution notes render via the shared `NoteComment` sub-row. + */ +export const EventDismissalRequest = () => ( +
    { + if ((e.target as HTMLElement).closest('a')) e.preventDefault() + }} + > + {/* Dismissal requested — circle user actor, attention/comment badge. When + `show_dismissal_actions` is true, the live inline template + (`dismissal_requested_component.rb`) renders a `` + holding `DismissalReviewDialogComponent` — whose visible control is a + SINGLE small primary "Review request" button that opens a review dialog + (the approve/deny choice lives inside the dialog, not on the row). We + place that right-aligned control in the `Timeline.Actions` slot. */} +
    +

    Dismissal requested

    + + + + + + + + {'requested to dismiss this as '} + Tolerable risk + + + + + +
    + + {/* Dismissal approved — circle user actor, success/check badge */} +
    +

    Dismissal approved

    + + + + + + + + {'approved dismissal '} + + + +
    + + {/* Dismissal denied — circle user actor, danger/x badge */} +
    +

    Dismissal denied

    + + + + + + + + {'denied dismissal '} + + + +
    + + {/* Dismissal cancelled — circle user actor, plain default (subtle) x badge. + `DismissalCancelledComponent` renders `with_badge(color: :subtle, icon: + "x")` (no bg) → a bare default badge, not a danger/emphasis one. */} +
    +

    Dismissal cancelled

    + + + + + + + + {'cancelled their dismissal request '} + + + +
    +
    +)