From fd366cec440c287267b13d94bcccd3315aa8d10a Mon Sep 17 00:00:00 2001
From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com>
Date: Thu, 20 Aug 2026 20:18:02 -0700
Subject: [PATCH 1/3] Add presentational Viewing-menu (Timeline filter) demo
story
Move the Playground surface data and pure derivation helpers out of
Timeline.stories.tsx into internal/timelinePlaygroundData so both the
Playground and the new filter demo share them. Behavior is preserved:
the Playground stories render identically.
Add Timeline.filter.features.stories.tsx: an ActionMenu multi-select
"Viewing" control that filters Timeline rows consumer-side. The opening
and closing lifecycle bookends stay pinned outside the predicate so the
timeline never renders empty regardless of toggles; the consumer owns
that guarantee, not the Timeline or control components.
Story and internal scaffolding only: no public API export, no changeset.
The authoritative Timeline taxonomy stays in github-ui.
---
...imeline.filter.features.stories.module.css | 4 +
.../Timeline.filter.features.stories.tsx | 206 +++++
.../src/Timeline/Timeline.stories.module.css | 9 +-
.../react/src/Timeline/Timeline.stories.tsx | 706 +-----------------
.../timelinePlaygroundData.module.css | 10 +
.../internal/timelinePlaygroundData.tsx | 701 +++++++++++++++++
6 files changed, 933 insertions(+), 703 deletions(-)
create mode 100644 packages/react/src/Timeline/Timeline.filter.features.stories.module.css
create mode 100644 packages/react/src/Timeline/Timeline.filter.features.stories.tsx
create mode 100644 packages/react/src/Timeline/internal/timelinePlaygroundData.module.css
create mode 100644 packages/react/src/Timeline/internal/timelinePlaygroundData.tsx
diff --git a/packages/react/src/Timeline/Timeline.filter.features.stories.module.css b/packages/react/src/Timeline/Timeline.filter.features.stories.module.css
new file mode 100644
index 00000000000..f76b0293ace
--- /dev/null
+++ b/packages/react/src/Timeline/Timeline.filter.features.stories.module.css
@@ -0,0 +1,4 @@
+/* Story-only styles for the Timeline "Viewing" filter demo. Not shipped. */
+.Toolbar {
+ margin-block-end: var(--base-size-16);
+}
diff --git a/packages/react/src/Timeline/Timeline.filter.features.stories.tsx b/packages/react/src/Timeline/Timeline.filter.features.stories.tsx
new file mode 100644
index 00000000000..38f529023ef
--- /dev/null
+++ b/packages/react/src/Timeline/Timeline.filter.features.stories.tsx
@@ -0,0 +1,206 @@
+import type {Meta} from '@storybook/react-vite'
+import {useState} from 'react'
+import {CheckCircleIcon, EyeIcon, IssueOpenedIcon} from '@primer/octicons-react'
+import type {ComponentProps} from '../utils/types'
+import {ActionList} from '../ActionList'
+import {ActionMenu} from '../ActionMenu'
+import {FeatureFlags} from '../FeatureFlags'
+import Timeline from './Timeline'
+import {Examples, MutedTime, UserActor} from './internal/timelineStoryHelpers'
+import {
+ PLAYGROUND_SURFACES,
+ playgroundCategoryIds,
+ playgroundEvents,
+ type PlaygroundCategoryId,
+} from './internal/timelinePlaygroundData'
+import classes from './Timeline.filter.features.stories.module.css'
+
+export default {
+ title: 'Components/Timeline/Filter',
+ component: Timeline,
+ subcomponents: {
+ 'Timeline.Item': Timeline.Item,
+ 'Timeline.Badge': Timeline.Badge,
+ 'Timeline.Body': Timeline.Body,
+ },
+ decorators: [
+ // File-scoped: render every story in the future-state list semantics
+ // (`
`/`
`), matching the other Timeline surface stories.
+ Story => (
+
+
+
+ ),
+ ],
+} as Meta>
+
+// The single representative surface for this demo. `issue` offers the richest
+// category set (status / references / moderation / metadata).
+const SURFACE_ID = 'issue' as const
+
+// Lifecycle types the consumer PINS as never-filtered bookends (see the story
+// notes). They are excluded from the filterable interior so a toggled-on
+// category never renders a duplicate opening/closing row.
+const BOOKEND_LIFECYCLE_TYPES = new Set(['opened', 'closed'])
+
+type ViewingOption = {id: PlaygroundCategoryId; label: string}
+
+/**
+ * Presentational "Viewing" control. Its entire contract is `options` /
+ * `selected` / `onSelectedChange`; it reads NO `data-*` attributes and knows
+ * nothing about the Timeline it filters. The consumer owns the predicate and
+ * decides which rows render. This mirrors the prototype's per-surface Viewing
+ * menu: a Primer `ActionMenu` multi-select (`selectionVariant="multiple"`) whose
+ * checklist items stay open on select and expose `role="menuitemcheckbox"` /
+ * `aria-checked` for screen readers. The trigger's accessible name is its text
+ * ("Viewing"); the eye icon is decorative.
+ */
+function ViewingFilterMenu({
+ options,
+ selected,
+ onSelectedChange,
+}: {
+ options: ViewingOption[]
+ selected: PlaygroundCategoryId[]
+ onSelectedChange: (next: PlaygroundCategoryId[]) => void
+}) {
+ return (
+
+ Viewing
+
+
+ {options.map(option => {
+ const isSelected = selected.includes(option.id)
+ return (
+
+ onSelectedChange(isSelected ? selected.filter(id => id !== option.id) : [...selected, option.id])
+ }
+ >
+ {option.label}
+
+ )
+ })}
+
+
+
+ )
+}
+
+/**
+ * Viewing menu (consumer-side category filtering).
+ *
+ * This is a PRESENTATIONAL demo of the prototype's per-surface "Viewing" menu. A
+ * Primer `ActionMenu` multi-select toggles which event CATEGORIES the selected
+ * surface renders. There is NO public `Timeline.Filter` API: the control
+ * (`ViewingFilterMenu`) is story-local scaffolding and filtering is 100%
+ * consumer-side.
+ *
+ * How the pieces divide responsibility:
+ * - The control is presentational. Its contract is `options` / `selected` /
+ * `onSelectedChange`. It does NOT read the `data-*` attributes on the rows.
+ * - The consumer (this story) owns the predicate. It filters the representative
+ * rows on each event's `category` and hands the survivors to `Timeline`.
+ * - `Timeline` and its slots stay taxonomy-agnostic. The `data-event-*`
+ * attributes ride along on `Timeline.Item` exactly as the Playground renders
+ * them; the control never inspects them.
+ *
+ * Never-empty guarantee (CONSUMER-owned): the opening and closing lifecycle
+ * events are PINNED outside the category predicate, so the timeline never
+ * renders empty no matter which categories are toggled off. This is the
+ * consumer's responsibility, NOT the Timeline or control components. Because the
+ * consumer pins these bookends, it also excludes their lifecycle types from the
+ * filterable interior (`BOOKEND_LIFECYCLE_TYPES`) so a toggled-on category can't
+ * duplicate them.
+ *
+ * The representative data is illustrative, github-flavored sample data only. The
+ * authoritative Timeline event taxonomy lives in github-ui as
+ * `@github-ui/timeline-taxonomy`; primer/react is taxonomy-agnostic.
+ */
+export const ViewingMenu = () => {
+ const surface = PLAYGROUND_SURFACES[SURFACE_ID]
+ const categoryIds = playgroundCategoryIds(SURFACE_ID)
+ const options: ViewingOption[] = categoryIds.map(id => ({
+ id,
+ label: surface.categories[id]?.label ?? id,
+ }))
+
+ // All categories start selected: nothing is filtered out initially.
+ const [selected, setSelected] = useState(categoryIds)
+
+ // Consumer-side predicate: keep rows whose category is still selected, minus
+ // the pinned lifecycle bookends.
+ const interior = playgroundEvents(SURFACE_ID, selected).filter(event => !BOOKEND_LIFECYCLE_TYPES.has(event.type))
+
+ return (
+
+
+
+
+
+ {/*
+ PINNED opening bookend — consumer-owned, never filtered. It carries no
+ `data-event-category`, so it sits outside the category axis the Viewing
+ menu toggles.
+ */}
+
+
+ {/* Decorative: the summary text in Timeline.Body is the accessible description. */}
+
+
+
+
+ {'opened this '}
+
+
+
+
+ {interior.map(event => {
+ const BadgeIcon = event.badge.icon
+ return (
+
+
+ {/* Decorative: the summary text in Timeline.Body is the accessible description. */}
+
+
+ {event.body}
+ {event.actions ? {event.actions} : null}
+
+ )
+ })}
+
+ {/* PINNED closing bookend — consumer-owned, never filtered. */}
+
+
+ {/* Decorative: the summary text in Timeline.Body is the accessible description. */}
+
+
+
+
+ {'closed this as completed '}
+
+
+
+
+
+ )
+}
diff --git a/packages/react/src/Timeline/Timeline.stories.module.css b/packages/react/src/Timeline/Timeline.stories.module.css
index beaf0839b4c..2945adf4784 100644
--- a/packages/react/src/Timeline/Timeline.stories.module.css
+++ b/packages/react/src/Timeline/Timeline.stories.module.css
@@ -52,16 +52,11 @@
}
/*
- * Timeline Playground: story-local styles for the representative event bodies and the empty
- * state rendered by the `TimelinePlayground` story (its controls are Storybook argTypes).
+ * Timeline Playground: story-local style for the empty state rendered by the
+ * `TimelinePlayground` story (its controls are Storybook argTypes).
*/
.PlaygroundEmpty {
max-width: 1012px;
color: var(--fgColor-muted);
}
-
-.Strong {
- font-weight: var(--base-text-weight-semibold);
- color: var(--fgColor-default);
-}
diff --git a/packages/react/src/Timeline/Timeline.stories.tsx b/packages/react/src/Timeline/Timeline.stories.tsx
index d77135d4b79..56db93ac39e 100644
--- a/packages/react/src/Timeline/Timeline.stories.tsx
+++ b/packages/react/src/Timeline/Timeline.stories.tsx
@@ -7,7 +7,6 @@ import {TimelineBadgeVariants} from './constants'
import Avatar from '../Avatar'
import {Button} from '../Button'
import Link from '../Link'
-import Label from '../Label'
import RelativeTime from '../RelativeTime'
import {
AlertIcon,
@@ -42,24 +41,18 @@ import {
TrashIcon,
UnlockIcon,
XCircleIcon,
- // Additional badge icons used by the Timeline Playground story below.
- BlockedIcon,
- CheckIcon,
- CommentIcon,
- DotFillIcon,
- type Icon,
- LinkExternalIcon,
- MarkGithubIcon,
- NoteIcon,
- ShieldCheckIcon,
- ShieldSlashIcon,
- ShieldXIcon,
- SyncIcon,
- XIcon,
} from '@primer/octicons-react'
import {FeatureFlags} from '../FeatureFlags'
import Text from '../Text'
-import {BoldLink, EventSubRow, Examples, MONALISA_AVATAR, MutedTime, UserActor} from './internal/timelineStoryHelpers'
+import {Examples} from './internal/timelineStoryHelpers'
+import {
+ PLAYGROUND_SURFACES,
+ PLAYGROUND_SURFACE_IDS,
+ playgroundCategoryIds,
+ playgroundEvents,
+ type PlaygroundCategoryId,
+ type PlaygroundSurfaceId,
+} from './internal/timelinePlaygroundData'
import classes from './Timeline.stories.module.css'
export default {
@@ -554,685 +547,6 @@ EventPlayground.argTypes = {
// Timeline Playground
// ============================================================================
-/**
- * ILLUSTRATIVE, REPRESENTATIVE DATA — read this before treating anything below as canonical.
- *
- * The **Timeline Playground** demonstrates how filtering `data-*` attributes
- * (`data-event-scope`, `data-event-type`, `data-event-category`, `data-event-visibility`,
- * `data-actor-type`) are embedded on each `Timeline.Item` across GitHub surfaces. It mirrors
- * the Figma prototype: a `surface` control picks a surface, a categories control filters by the
- * categories that surface offers, and an event-types control selects which event types render.
- * Changing the surface reveals that surface's category and event-type controls and hides the
- * others.
- *
- * The `PLAYGROUND_SURFACES` map below is a small, hardcoded, story-local sample of
- * github-flavored events (a handful per surface, NOT the full ~160-row catalog). Its
- * surface/category/type shape and every copy string are ILLUSTRATIVE examples for this demo
- * only. The AUTHORITATIVE, per-surface timeline taxonomy is a GitHub product concern owned by
- * `github-ui` (the product repositories that render these timelines). Primer does not host an
- * authoritative taxonomy; any internal Primer taxonomy code is non-authoritative and not public
- * API. Do not treat this inline map as the real catalog, do not export it, and do not promote it
- * into a reusable module — it is intentionally confined to this story file.
- *
- * The picker is structured so a future `Timeline.Filter` can drive it: the render pipeline
- * derives `visibleRows` from the selected categories and types, then maps each row to a
- * ``.
- */
-
-type PlaygroundSurfaceId = 'code-scanning' | 'secret-scanning' | 'dependabot' | 'license-compliance' | 'issue'
-type PlaygroundCategoryId = 'findings' | 'status' | 'reviews' | 'references' | 'moderation' | 'metadata'
-// Actor classification, mirrors the authoritative `ActorType` value space (`user | bot`):
-// first-party automation such as the GitHub secret-scanning system actor is `bot`.
-type PlaygroundActorType = 'user' | 'bot'
-
-type PlaygroundEvent = {
- /** `data-event-type` value */
- type: string
- /** `data-event-category` value */
- category: PlaygroundCategoryId
- /** Human-readable label shown in the event-type picker */
- label: string
- /**
- * `data-event-visibility` value. Matches the authoritative `EventVisibility` value space
- * (`@github-ui/timeline-taxonomy`): `primary` rows render in the main timeline; `auditOnly`
- * rows are metadata-only.
- */
- visibility: 'primary' | 'auditOnly'
- /** `data-actor-type` value; omit for actor-less rows so no `data-actor-type` attribute renders */
- actorType?: PlaygroundActorType
- badge: {icon: Icon; variant?: TimelineBadgeVariant}
- /** Contents of `Timeline.Body` */
- body: React.ReactNode
- /** Optional contents of the right-aligned `Timeline.Actions` slot */
- actions?: React.ReactNode
-}
-
-type PlaygroundSurface = {
- label: string
- /** Accessible name for the rendered `` */
- ariaLabel: string
- categories: Partial>
-}
-
-// Story-local demo avatars (MONALISA_AVATAR is imported from the shared helpers).
-const DEPENDABOT_BOT_AVATAR = 'https://avatars.githubusercontent.com/u/27347476?v=4'
-const LICENSE_BOT_AVATAR = 'https://avatars.githubusercontent.com/u/9919?s=40&v=4'
-const HUBOT_AVATAR = 'https://avatars.githubusercontent.com/u/480938?v=4'
-
-// ILLUSTRATIVE representative data (see the canon note above). Each event mirrors the
-// badge/icon/copy/actor of the matching VariantSection in that surface's existing
-// `Timeline..features.stories.tsx`, reduced to a demonstrative subset. The four
-// security surfaces offer findings/status/reviews; issues offers status/references/moderation.
-const PLAYGROUND_SURFACES: Record = {
- 'code-scanning': {
- label: 'Code scanning',
- ariaLabel: 'Code scanning alert timeline',
- categories: {
- findings: {
- label: 'Findings',
- events: [
- {
- type: 'detected',
- category: 'findings',
- label: 'First detected in commit',
- visibility: 'primary',
- badge: {icon: ShieldIcon},
- body: (
- <>
- First detected in commit{' '}
-
- >
- ),
- },
- {
- type: 'fixed',
- category: 'findings',
- label: 'Fixed in branch',
- visibility: 'primary',
- badge: {icon: ShieldCheckIcon, variant: 'done'},
- body: (
- <>
- Fixed in branchmain{' '}
-
- >
- ),
- },
- ],
- },
- status: {
- label: 'Status',
- events: [
- {
- type: 'closed',
- category: 'status',
- label: 'Closed as false positive',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: ShieldXIcon, variant: 'danger'},
- body: (
- <>
-
- {'closed this as '}
- false positive{' '}
-
- >
- ),
- },
- {
- type: 'reopened',
- category: 'status',
- label: 'Reopened',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: DotFillIcon, variant: 'success'},
- body: (
- <>
-
- {'reopened this '}
-
- >
- ),
- },
- ],
- },
- reviews: {
- label: 'Reviews',
- events: [
- {
- type: 'dismissal_requested',
- category: 'reviews',
- label: 'Requested to dismiss',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: CommentIcon},
- body: (
- <>
-
- {'requested to dismiss this as false positive '}
-
- This finding is a test-only helper, safe to dismiss.
- >
- ),
- actions: (
-
- ),
- },
- {
- type: 'dismissal_reviewed',
- category: 'reviews',
- label: 'Approved dismissal',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: CheckIcon},
- body: (
- <>
-
- {'approved dismissal '}
-
- >
- ),
- },
- ],
- },
- },
- },
- 'secret-scanning': {
- label: 'Secret scanning',
- ariaLabel: 'Secret scanning alert timeline',
- categories: {
- findings: {
- label: 'Findings',
- events: [
- {
- type: 'detected',
- category: 'findings',
- label: 'Created',
- visibility: 'primary',
- actorType: 'bot',
- badge: {icon: ShieldIcon, variant: 'success'},
- body: (
- <>
-
- {'opened this alert '}
-
- >
- ),
- },
- {
- type: 'validity_active',
- category: 'findings',
- label: 'Validity: active',
- visibility: 'primary',
- actorType: 'bot',
- badge: {icon: AlertIcon, variant: 'danger'},
- body: (
- <>
-
- {'verified this secret is active '}
-
- >
- ),
- },
- ],
- },
- status: {
- label: 'Status',
- events: [
- {
- type: 'closed',
- category: 'status',
- label: 'Closed as revoked',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: ShieldCheckIcon, variant: 'done'},
- body: (
- <>
-
- {'closed this as '}
- revoked
-
- Rotated the leaked token and confirmed the provider revoked it.
-
- >
- ),
- },
- {
- type: 'reopened',
- category: 'status',
- label: 'Reopened',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: SyncIcon, variant: 'success'},
- body: (
- <>
-
- {'reopened this '}
-
- >
- ),
- },
- ],
- },
- reviews: {
- label: 'Reviews',
- events: [
- {
- type: 'closure_requested',
- category: 'reviews',
- label: 'Requested to dismiss',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: CommentIcon},
- body: (
- <>
-
- {'requested to dismiss this as false positive '}
-
- >
- ),
- actions: (
-
- ),
- },
- {
- type: 'bypass_approved',
- category: 'reviews',
- label: 'Bypass approved',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: CheckCircleIcon},
- body: (
- <>
-
- {'approved a bypass '}
-
- >
- ),
- },
- ],
- },
- },
- },
- dependabot: {
- label: 'Dependabot',
- ariaLabel: 'Dependabot alert timeline',
- categories: {
- findings: {
- label: 'Findings',
- events: [
- {
- type: 'opened',
- category: 'findings',
- label: 'Opened',
- visibility: 'primary',
- actorType: 'bot',
- badge: {icon: ShieldIcon, variant: 'success'},
- body: (
- <>
-
- {'opened this '}
-
- >
- ),
- },
- {
- type: 'fixed',
- category: 'findings',
- label: 'Fixed',
- visibility: 'primary',
- actorType: 'bot',
- badge: {icon: ShieldCheckIcon, variant: 'done'},
- body: (
- <>
-
- {'closed this as completed '}
-
- >
- ),
- },
- ],
- },
- status: {
- label: 'Status',
- events: [
- {
- type: 'reopened',
- category: 'status',
- label: 'Reopened',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: SyncIcon, variant: 'success'},
- body: (
- <>
-
- {'reopened this '}
-
- >
- ),
- },
- {
- type: 'dismissed',
- category: 'status',
- label: 'Dismissed',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: ShieldSlashIcon},
- body: (
- <>
-
- {'dismissed this as '}
- risk is tolerable{' '}
-
- Only reachable from a dev-only script we do not ship.
- >
- ),
- },
- ],
- },
- reviews: {
- label: 'Reviews',
- events: [
- {
- type: 'dismissal_requested',
- category: 'reviews',
- label: 'Dismissal requested',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: CommentIcon},
- body: (
- <>
-
- {'requested to dismiss this '}
-
- >
- ),
- actions: (
-
- ),
- },
- ],
- },
- },
- },
- 'license-compliance': {
- label: 'License compliance',
- ariaLabel: 'License compliance alert timeline',
- categories: {
- findings: {
- label: 'Findings',
- events: [
- {
- type: 'opened',
- category: 'findings',
- label: 'Opened',
- visibility: 'primary',
- actorType: 'bot',
- badge: {icon: ShieldIcon, variant: 'success'},
- body: (
- <>
-
- {' opened this alert '}
-
- >
- ),
- },
- {
- type: 'appeared_in_branch',
- category: 'findings',
- label: 'Appeared in branch',
- visibility: 'primary',
- badge: {icon: GitBranchIcon},
- body: (
- <>
- {'Appeared in branch '}
- feature-branch{' '}
-
- >
- ),
- },
- ],
- },
- status: {
- label: 'Status',
- events: [
- {
- type: 'closed',
- category: 'status',
- label: 'Closed as amendment',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: ShieldCheckIcon, variant: 'done'},
- body: (
- <>
-
- {' closed as amendment '}
-
- Added a policy exception covering this package.
- >
- ),
- },
- ],
- },
- reviews: {
- label: 'Reviews',
- events: [
- {
- type: 'review_requested',
- category: 'reviews',
- label: 'Requested to close',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: CommentIcon},
- body: (
- <>
-
- {' requested to close '}
-
- >
- ),
- actions: (
-
- ),
- },
- {
- type: 'review_approved',
- category: 'reviews',
- label: 'Approved closure request',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: CheckIcon},
- body: (
- <>
-
- {' approved closure request '}
-
- >
- ),
- },
- {
- type: 'review_denied',
- category: 'reviews',
- label: 'Denied closure request',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: XIcon},
- body: (
- <>
-
- {' denied closure request '}
-
- >
- ),
- },
- ],
- },
- },
- },
- issue: {
- label: 'Issues',
- ariaLabel: 'Issue timeline',
- categories: {
- status: {
- label: 'Status',
- events: [
- {
- type: 'closed',
- category: 'status',
- label: 'Closed as completed',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: CheckCircleIcon, variant: 'done'},
- body: (
- <>
-
- {'closed this as '}
-
- completed
- {' '}
-
- >
- ),
- },
- {
- type: 'reopened',
- category: 'status',
- label: 'Reopened',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: IssueReopenedIcon, variant: 'open'},
- body: (
- <>
-
- {'reopened this '}
-
- >
- ),
- },
- ],
- },
- references: {
- label: 'References',
- events: [
- {
- type: 'connected',
- category: 'references',
- label: 'Linked pull request',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: CrossReferenceIcon},
- body: (
- <>
-
- {'linked a pull request that will close this issue '}
- Add retry logic to the uploader
- {' #42 '}
-
- >
- ),
- },
- {
- type: 'cross_referenced',
- category: 'references',
- label: 'Mentioned in an issue',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: LinkExternalIcon},
- body: (
- <>
-
- {'mentioned this '}
-
-
-
- Track flaky upload retries
-
- {' #128'}
-
- >
- ),
- },
- ],
- },
- moderation: {
- label: 'Moderation',
- events: [
- {
- type: 'user_blocked',
- category: 'moderation',
- label: 'User blocked',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: BlockedIcon},
- body: (
- <>
-
- {'blocked '}
- six7
- >
- ),
- },
- {
- type: 'comment_pinned',
- category: 'moderation',
- label: 'Comment pinned',
- visibility: 'primary',
- actorType: 'user',
- badge: {icon: PinIcon},
- body: (
- <>
-
- {'pinned a '}
-
- comment
- {' '}
-
- >
- ),
- },
- ],
- },
- metadata: {
- label: 'Metadata',
- events: [
- {
- type: 'labeled',
- category: 'metadata',
- label: 'Labeled (audit only)',
- visibility: 'auditOnly',
- actorType: 'user',
- badge: {icon: TagIcon},
- body: (
- <>
-
- {'added the '}
-
- {' label '}
-
- >
- ),
- },
- ],
- },
- },
- },
-}
-
-const PLAYGROUND_SURFACE_IDS = Object.keys(PLAYGROUND_SURFACES) as PlaygroundSurfaceId[]
-
-const playgroundCategoryIds = (surface: PlaygroundSurfaceId): PlaygroundCategoryId[] =>
- Object.keys(PLAYGROUND_SURFACES[surface].categories) as PlaygroundCategoryId[]
-
-const playgroundEvents = (surface: PlaygroundSurfaceId, categories: PlaygroundCategoryId[]): PlaygroundEvent[] =>
- categories.flatMap(category => PLAYGROUND_SURFACES[surface].categories[category]?.events ?? [])
-
/**
* Args for the {@link TimelinePlayground} story. A single `surface` selector, plus a categories
* multi-select and an event-types multi-select for each of the five surfaces. Only the selected
@@ -1301,7 +615,7 @@ const playgroundTypeControl = (surface: PlaygroundSurfaceId) => {
}
/**
- * The **Timeline Playground** (see the canon note above for the illustrative-data caveat).
+ * The **Timeline Playground** (see the illustrative-data note in `internal/timelinePlaygroundData.tsx`).
*
* The picker is built from real Storybook controls, not in-canvas form elements. Storybook cannot
* repopulate one control's options from another control's value, so instead of a single dependent
diff --git a/packages/react/src/Timeline/internal/timelinePlaygroundData.module.css b/packages/react/src/Timeline/internal/timelinePlaygroundData.module.css
new file mode 100644
index 00000000000..1ec9c063c58
--- /dev/null
+++ b/packages/react/src/Timeline/internal/timelinePlaygroundData.module.css
@@ -0,0 +1,10 @@
+/*
+ * Story-local styles for the representative Timeline event bodies in
+ * `timelinePlaygroundData.tsx`. Shared by the Timeline Playground story and the
+ * Timeline "Viewing" filter story so the emphasized inline text renders identically.
+ */
+
+.Strong {
+ font-weight: var(--base-text-weight-semibold);
+ color: var(--fgColor-default);
+}
diff --git a/packages/react/src/Timeline/internal/timelinePlaygroundData.tsx b/packages/react/src/Timeline/internal/timelinePlaygroundData.tsx
new file mode 100644
index 00000000000..d9dffed2611
--- /dev/null
+++ b/packages/react/src/Timeline/internal/timelinePlaygroundData.tsx
@@ -0,0 +1,701 @@
+/**
+ * Internal, story-only Timeline "playground" sample data. Not part of the public
+ * API (stories don't ship) and intentionally NOT exported from the package index.
+ *
+ * `PLAYGROUND_SURFACES` is an ILLUSTRATIVE, github-flavored map of representative
+ * event surfaces/categories used only to drive the Timeline Playground and the
+ * Timeline "Viewing" filter stories. It is NOT an authoritative taxonomy.
+ *
+ * The canonical Timeline event taxonomy lives in github-ui as
+ * `@github-ui/timeline-taxonomy`. primer/react is taxonomy-agnostic: do not treat
+ * this sample as a source of truth, do not promote it into an exported reusable
+ * taxonomy module, and do not add it to `packages/react/src/index.ts`. It stays
+ * confined to internal story scaffolding (alongside `timelineStoryHelpers.tsx`) so
+ * the surfaces stay consistent across stories without leaking into the public
+ * package. Keep it in sync with github-ui only as needed for the demos.
+ */
+import type React from 'react'
+import {
+ AlertIcon,
+ BlockedIcon,
+ CheckCircleIcon,
+ CheckIcon,
+ CommentIcon,
+ CrossReferenceIcon,
+ DotFillIcon,
+ GitBranchIcon,
+ type Icon,
+ IssueOpenedIcon,
+ IssueReopenedIcon,
+ LinkExternalIcon,
+ MarkGithubIcon,
+ NoteIcon,
+ PinIcon,
+ ShieldCheckIcon,
+ ShieldIcon,
+ ShieldSlashIcon,
+ ShieldXIcon,
+ SyncIcon,
+ TagIcon,
+ XIcon,
+} from '@primer/octicons-react'
+import {Button} from '../../Button'
+import Label from '../../Label'
+import Link from '../../Link'
+import {type TimelineBadgeVariant} from '../Timeline'
+import {BoldLink, EventSubRow, MONALISA_AVATAR, MutedTime, UserActor} from './timelineStoryHelpers'
+import classes from './timelinePlaygroundData.module.css'
+
+export type PlaygroundSurfaceId = 'code-scanning' | 'secret-scanning' | 'dependabot' | 'license-compliance' | 'issue'
+export type PlaygroundCategoryId = 'findings' | 'status' | 'reviews' | 'references' | 'moderation' | 'metadata'
+// Actor classification, mirrors the authoritative `ActorType` value space (`user | bot`):
+// first-party automation such as the GitHub secret-scanning system actor is `bot`.
+type PlaygroundActorType = 'user' | 'bot'
+
+export type PlaygroundEvent = {
+ /** `data-event-type` value */
+ type: string
+ /** `data-event-category` value */
+ category: PlaygroundCategoryId
+ /** Human-readable label shown in the event-type picker */
+ label: string
+ /**
+ * `data-event-visibility` value. Matches the authoritative `EventVisibility` value space
+ * (`@github-ui/timeline-taxonomy`): `primary` rows render in the main timeline; `auditOnly`
+ * rows are metadata-only.
+ */
+ visibility: 'primary' | 'auditOnly'
+ /** `data-actor-type` value; omit for actor-less rows so no `data-actor-type` attribute renders */
+ actorType?: PlaygroundActorType
+ badge: {icon: Icon; variant?: TimelineBadgeVariant}
+ /** Contents of `Timeline.Body` */
+ body: React.ReactNode
+ /** Optional contents of the right-aligned `Timeline.Actions` slot */
+ actions?: React.ReactNode
+}
+
+export type PlaygroundSurface = {
+ label: string
+ /** Accessible name for the rendered `` */
+ ariaLabel: string
+ categories: Partial>
+}
+
+// Story-local demo avatars (MONALISA_AVATAR is imported from the shared helpers).
+const DEPENDABOT_BOT_AVATAR = 'https://avatars.githubusercontent.com/u/27347476?v=4'
+const LICENSE_BOT_AVATAR = 'https://avatars.githubusercontent.com/u/9919?s=40&v=4'
+const HUBOT_AVATAR = 'https://avatars.githubusercontent.com/u/480938?v=4'
+
+// ILLUSTRATIVE representative data (see the module header above). Each event mirrors the
+// badge/icon/copy/actor of the matching VariantSection in that surface's existing
+// `Timeline..features.stories.tsx`, reduced to a demonstrative subset. The four
+// security surfaces offer findings/status/reviews; issues offers status/references/moderation/metadata.
+export const PLAYGROUND_SURFACES: Record = {
+ 'code-scanning': {
+ label: 'Code scanning',
+ ariaLabel: 'Code scanning alert timeline',
+ categories: {
+ findings: {
+ label: 'Findings',
+ events: [
+ {
+ type: 'detected',
+ category: 'findings',
+ label: 'First detected in commit',
+ visibility: 'primary',
+ badge: {icon: ShieldIcon},
+ body: (
+ <>
+ First detected in commit{' '}
+
+ >
+ ),
+ },
+ {
+ type: 'fixed',
+ category: 'findings',
+ label: 'Fixed in branch',
+ visibility: 'primary',
+ badge: {icon: ShieldCheckIcon, variant: 'done'},
+ body: (
+ <>
+ Fixed in branchmain{' '}
+
+ >
+ ),
+ },
+ ],
+ },
+ status: {
+ label: 'Status',
+ events: [
+ {
+ type: 'closed',
+ category: 'status',
+ label: 'Closed as false positive',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: ShieldXIcon, variant: 'danger'},
+ body: (
+ <>
+
+ {'closed this as '}
+ false positive{' '}
+
+ >
+ ),
+ },
+ {
+ type: 'reopened',
+ category: 'status',
+ label: 'Reopened',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: DotFillIcon, variant: 'success'},
+ body: (
+ <>
+
+ {'reopened this '}
+
+ >
+ ),
+ },
+ ],
+ },
+ reviews: {
+ label: 'Reviews',
+ events: [
+ {
+ type: 'dismissal_requested',
+ category: 'reviews',
+ label: 'Requested to dismiss',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: CommentIcon},
+ body: (
+ <>
+
+ {'requested to dismiss this as false positive '}
+
+ This finding is a test-only helper, safe to dismiss.
+ >
+ ),
+ actions: (
+
+ ),
+ },
+ {
+ type: 'dismissal_reviewed',
+ category: 'reviews',
+ label: 'Approved dismissal',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: CheckIcon},
+ body: (
+ <>
+
+ {'approved dismissal '}
+
+ >
+ ),
+ },
+ ],
+ },
+ },
+ },
+ 'secret-scanning': {
+ label: 'Secret scanning',
+ ariaLabel: 'Secret scanning alert timeline',
+ categories: {
+ findings: {
+ label: 'Findings',
+ events: [
+ {
+ type: 'detected',
+ category: 'findings',
+ label: 'Created',
+ visibility: 'primary',
+ actorType: 'bot',
+ badge: {icon: ShieldIcon, variant: 'success'},
+ body: (
+ <>
+
+ {'opened this alert '}
+
+ >
+ ),
+ },
+ {
+ type: 'validity_active',
+ category: 'findings',
+ label: 'Validity: active',
+ visibility: 'primary',
+ actorType: 'bot',
+ badge: {icon: AlertIcon, variant: 'danger'},
+ body: (
+ <>
+
+ {'verified this secret is active '}
+
+ >
+ ),
+ },
+ ],
+ },
+ status: {
+ label: 'Status',
+ events: [
+ {
+ type: 'closed',
+ category: 'status',
+ label: 'Closed as revoked',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: ShieldCheckIcon, variant: 'done'},
+ body: (
+ <>
+
+ {'closed this as '}
+ revoked
+
+ Rotated the leaked token and confirmed the provider revoked it.
+
+ >
+ ),
+ },
+ {
+ type: 'reopened',
+ category: 'status',
+ label: 'Reopened',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: SyncIcon, variant: 'success'},
+ body: (
+ <>
+
+ {'reopened this '}
+
+ >
+ ),
+ },
+ ],
+ },
+ reviews: {
+ label: 'Reviews',
+ events: [
+ {
+ type: 'closure_requested',
+ category: 'reviews',
+ label: 'Requested to dismiss',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: CommentIcon},
+ body: (
+ <>
+
+ {'requested to dismiss this as false positive '}
+
+ >
+ ),
+ actions: (
+
+ ),
+ },
+ {
+ type: 'bypass_approved',
+ category: 'reviews',
+ label: 'Bypass approved',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: CheckCircleIcon},
+ body: (
+ <>
+
+ {'approved a bypass '}
+
+ >
+ ),
+ },
+ ],
+ },
+ },
+ },
+ dependabot: {
+ label: 'Dependabot',
+ ariaLabel: 'Dependabot alert timeline',
+ categories: {
+ findings: {
+ label: 'Findings',
+ events: [
+ {
+ type: 'opened',
+ category: 'findings',
+ label: 'Opened',
+ visibility: 'primary',
+ actorType: 'bot',
+ badge: {icon: ShieldIcon, variant: 'success'},
+ body: (
+ <>
+
+ {'opened this '}
+
+ >
+ ),
+ },
+ {
+ type: 'fixed',
+ category: 'findings',
+ label: 'Fixed',
+ visibility: 'primary',
+ actorType: 'bot',
+ badge: {icon: ShieldCheckIcon, variant: 'done'},
+ body: (
+ <>
+
+ {'closed this as completed '}
+
+ >
+ ),
+ },
+ ],
+ },
+ status: {
+ label: 'Status',
+ events: [
+ {
+ type: 'reopened',
+ category: 'status',
+ label: 'Reopened',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: SyncIcon, variant: 'success'},
+ body: (
+ <>
+
+ {'reopened this '}
+
+ >
+ ),
+ },
+ {
+ type: 'dismissed',
+ category: 'status',
+ label: 'Dismissed',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: ShieldSlashIcon},
+ body: (
+ <>
+
+ {'dismissed this as '}
+ risk is tolerable{' '}
+
+ Only reachable from a dev-only script we do not ship.
+ >
+ ),
+ },
+ ],
+ },
+ reviews: {
+ label: 'Reviews',
+ events: [
+ {
+ type: 'dismissal_requested',
+ category: 'reviews',
+ label: 'Dismissal requested',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: CommentIcon},
+ body: (
+ <>
+
+ {'requested to dismiss this '}
+
+ >
+ ),
+ actions: (
+
+ ),
+ },
+ ],
+ },
+ },
+ },
+ 'license-compliance': {
+ label: 'License compliance',
+ ariaLabel: 'License compliance alert timeline',
+ categories: {
+ findings: {
+ label: 'Findings',
+ events: [
+ {
+ type: 'opened',
+ category: 'findings',
+ label: 'Opened',
+ visibility: 'primary',
+ actorType: 'bot',
+ badge: {icon: ShieldIcon, variant: 'success'},
+ body: (
+ <>
+
+ {' opened this alert '}
+
+ >
+ ),
+ },
+ {
+ type: 'appeared_in_branch',
+ category: 'findings',
+ label: 'Appeared in branch',
+ visibility: 'primary',
+ badge: {icon: GitBranchIcon},
+ body: (
+ <>
+ {'Appeared in branch '}
+ feature-branch{' '}
+
+ >
+ ),
+ },
+ ],
+ },
+ status: {
+ label: 'Status',
+ events: [
+ {
+ type: 'closed',
+ category: 'status',
+ label: 'Closed as amendment',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: ShieldCheckIcon, variant: 'done'},
+ body: (
+ <>
+
+ {' closed as amendment '}
+
+ Added a policy exception covering this package.
+ >
+ ),
+ },
+ ],
+ },
+ reviews: {
+ label: 'Reviews',
+ events: [
+ {
+ type: 'review_requested',
+ category: 'reviews',
+ label: 'Requested to close',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: CommentIcon},
+ body: (
+ <>
+
+ {' requested to close '}
+
+ >
+ ),
+ actions: (
+
+ ),
+ },
+ {
+ type: 'review_approved',
+ category: 'reviews',
+ label: 'Approved closure request',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: CheckIcon},
+ body: (
+ <>
+
+ {' approved closure request '}
+
+ >
+ ),
+ },
+ {
+ type: 'review_denied',
+ category: 'reviews',
+ label: 'Denied closure request',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: XIcon},
+ body: (
+ <>
+
+ {' denied closure request '}
+
+ >
+ ),
+ },
+ ],
+ },
+ },
+ },
+ issue: {
+ label: 'Issues',
+ ariaLabel: 'Issue timeline',
+ categories: {
+ status: {
+ label: 'Status',
+ events: [
+ {
+ type: 'closed',
+ category: 'status',
+ label: 'Closed as completed',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: CheckCircleIcon, variant: 'done'},
+ body: (
+ <>
+
+ {'closed this as '}
+
+ completed
+ {' '}
+
+ >
+ ),
+ },
+ {
+ type: 'reopened',
+ category: 'status',
+ label: 'Reopened',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: IssueReopenedIcon, variant: 'open'},
+ body: (
+ <>
+
+ {'reopened this '}
+
+ >
+ ),
+ },
+ ],
+ },
+ references: {
+ label: 'References',
+ events: [
+ {
+ type: 'connected',
+ category: 'references',
+ label: 'Linked pull request',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: CrossReferenceIcon},
+ body: (
+ <>
+
+ {'linked a pull request that will close this issue '}
+ Add retry logic to the uploader
+ {' #42 '}
+
+ >
+ ),
+ },
+ {
+ type: 'cross_referenced',
+ category: 'references',
+ label: 'Mentioned in an issue',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: LinkExternalIcon},
+ body: (
+ <>
+
+ {'mentioned this '}
+
+
+
+ Track flaky upload retries
+
+ {' #128'}
+
+ >
+ ),
+ },
+ ],
+ },
+ moderation: {
+ label: 'Moderation',
+ events: [
+ {
+ type: 'user_blocked',
+ category: 'moderation',
+ label: 'User blocked',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: BlockedIcon},
+ body: (
+ <>
+
+ {'blocked '}
+ six7
+ >
+ ),
+ },
+ {
+ type: 'comment_pinned',
+ category: 'moderation',
+ label: 'Comment pinned',
+ visibility: 'primary',
+ actorType: 'user',
+ badge: {icon: PinIcon},
+ body: (
+ <>
+
+ {'pinned a '}
+
+ comment
+ {' '}
+
+ >
+ ),
+ },
+ ],
+ },
+ metadata: {
+ label: 'Metadata',
+ events: [
+ {
+ type: 'labeled',
+ category: 'metadata',
+ label: 'Labeled (audit only)',
+ visibility: 'auditOnly',
+ actorType: 'user',
+ badge: {icon: TagIcon},
+ body: (
+ <>
+
+ {'added the '}
+
+ {' label '}
+
+ >
+ ),
+ },
+ ],
+ },
+ },
+ },
+}
+
+export const PLAYGROUND_SURFACE_IDS = Object.keys(PLAYGROUND_SURFACES) as PlaygroundSurfaceId[]
+
+export const playgroundCategoryIds = (surface: PlaygroundSurfaceId): PlaygroundCategoryId[] =>
+ Object.keys(PLAYGROUND_SURFACES[surface].categories) as PlaygroundCategoryId[]
+
+export const playgroundEvents = (surface: PlaygroundSurfaceId, categories: PlaygroundCategoryId[]): PlaygroundEvent[] =>
+ categories.flatMap(category => PLAYGROUND_SURFACES[surface].categories[category]?.events ?? [])
From 4ed9540ea0d3260005f266eec8db2c67b514431a Mon Sep 17 00:00:00 2001
From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com>
Date: Thu, 20 Aug 2026 21:33:59 -0700
Subject: [PATCH 2/3] Refine Viewing-menu demo: Features title, Strong helper,
descriptions
Retitle the filter story to Components/Timeline/Features with export WithFiltering so it reads as a feature demo rather than implying a shipped Timeline.Filter subcomponent.
Move the semibold emphasis into a shared Strong helper in timelineStoryHelpers (mirroring BoldLink) and delete the standalone timelinePlaygroundData.module.css. The default color is load-bearing over the muted Timeline body, so the class stays rather than inlining bare Text.
Add representative per-category descriptions in the Viewing menu, rendered through ActionList.Description (block variant). The authoritative per-category text lives in github-ui timeline-taxonomy; Primer renders the slot only.
Stories and internal scaffolding only: no public API export, no changeset.
---
.../Timeline.filter.features.stories.tsx | 22 +++++++++-----
.../timelinePlaygroundData.module.css | 10 -------
.../internal/timelinePlaygroundData.tsx | 30 +++++++++++--------
.../internal/timelineStoryHelpers.module.css | 7 +++++
.../internal/timelineStoryHelpers.tsx | 9 ++++++
5 files changed, 47 insertions(+), 31 deletions(-)
delete mode 100644 packages/react/src/Timeline/internal/timelinePlaygroundData.module.css
diff --git a/packages/react/src/Timeline/Timeline.filter.features.stories.tsx b/packages/react/src/Timeline/Timeline.filter.features.stories.tsx
index 38f529023ef..f8e840d180c 100644
--- a/packages/react/src/Timeline/Timeline.filter.features.stories.tsx
+++ b/packages/react/src/Timeline/Timeline.filter.features.stories.tsx
@@ -16,7 +16,7 @@ import {
import classes from './Timeline.filter.features.stories.module.css'
export default {
- title: 'Components/Timeline/Filter',
+ title: 'Components/Timeline/Features',
component: Timeline,
subcomponents: {
'Timeline.Item': Timeline.Item,
@@ -43,17 +43,19 @@ const SURFACE_ID = 'issue' as const
// category never renders a duplicate opening/closing row.
const BOOKEND_LIFECYCLE_TYPES = new Set(['opened', 'closed'])
-type ViewingOption = {id: PlaygroundCategoryId; label: string}
+type ViewingOption = {id: PlaygroundCategoryId; label: string; description?: string}
/**
* Presentational "Viewing" control. Its entire contract is `options` /
* `selected` / `onSelectedChange`; it reads NO `data-*` attributes and knows
* nothing about the Timeline it filters. The consumer owns the predicate and
- * decides which rows render. This mirrors the prototype's per-surface Viewing
- * menu: a Primer `ActionMenu` multi-select (`selectionVariant="multiple"`) whose
- * checklist items stay open on select and expose `role="menuitemcheckbox"` /
- * `aria-checked` for screen readers. The trigger's accessible name is its text
- * ("Viewing"); the eye icon is decorative.
+ * decides which rows render. Each option may also carry an optional
+ * consumer-supplied `description`, rendered as an `ActionList.Description` block
+ * under the label; the control authors none of this text. This mirrors the
+ * prototype's per-surface Viewing menu: a Primer `ActionMenu` multi-select
+ * (`selectionVariant="multiple"`) whose checklist items stay open on select and
+ * expose `role="menuitemcheckbox"` / `aria-checked` for screen readers. The
+ * trigger's accessible name is its text ("Viewing"); the eye icon is decorative.
*/
function ViewingFilterMenu({
options,
@@ -80,6 +82,9 @@ function ViewingFilterMenu({
}
>
{option.label}
+ {option.description ? (
+ {option.description}
+ ) : null}
)
})}
@@ -119,12 +124,13 @@ function ViewingFilterMenu({
* authoritative Timeline event taxonomy lives in github-ui as
* `@github-ui/timeline-taxonomy`; primer/react is taxonomy-agnostic.
*/
-export const ViewingMenu = () => {
+export const WithFiltering = () => {
const surface = PLAYGROUND_SURFACES[SURFACE_ID]
const categoryIds = playgroundCategoryIds(SURFACE_ID)
const options: ViewingOption[] = categoryIds.map(id => ({
id,
label: surface.categories[id]?.label ?? id,
+ description: surface.categories[id]?.description,
}))
// All categories start selected: nothing is filtered out initially.
diff --git a/packages/react/src/Timeline/internal/timelinePlaygroundData.module.css b/packages/react/src/Timeline/internal/timelinePlaygroundData.module.css
deleted file mode 100644
index 1ec9c063c58..00000000000
--- a/packages/react/src/Timeline/internal/timelinePlaygroundData.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-/*
- * Story-local styles for the representative Timeline event bodies in
- * `timelinePlaygroundData.tsx`. Shared by the Timeline Playground story and the
- * Timeline "Viewing" filter story so the emphasized inline text renders identically.
- */
-
-.Strong {
- font-weight: var(--base-text-weight-semibold);
- color: var(--fgColor-default);
-}
diff --git a/packages/react/src/Timeline/internal/timelinePlaygroundData.tsx b/packages/react/src/Timeline/internal/timelinePlaygroundData.tsx
index d9dffed2611..f0baffa3bc7 100644
--- a/packages/react/src/Timeline/internal/timelinePlaygroundData.tsx
+++ b/packages/react/src/Timeline/internal/timelinePlaygroundData.tsx
@@ -43,8 +43,7 @@ import {Button} from '../../Button'
import Label from '../../Label'
import Link from '../../Link'
import {type TimelineBadgeVariant} from '../Timeline'
-import {BoldLink, EventSubRow, MONALISA_AVATAR, MutedTime, UserActor} from './timelineStoryHelpers'
-import classes from './timelinePlaygroundData.module.css'
+import {BoldLink, EventSubRow, MONALISA_AVATAR, MutedTime, Strong, UserActor} from './timelineStoryHelpers'
export type PlaygroundSurfaceId = 'code-scanning' | 'secret-scanning' | 'dependabot' | 'license-compliance' | 'issue'
export type PlaygroundCategoryId = 'findings' | 'status' | 'reviews' | 'references' | 'moderation' | 'metadata'
@@ -78,7 +77,7 @@ export type PlaygroundSurface = {
label: string
/** Accessible name for the rendered `` */
ariaLabel: string
- categories: Partial>
+ categories: Partial>
}
// Story-local demo avatars (MONALISA_AVATAR is imported from the shared helpers).
@@ -106,8 +105,7 @@ export const PLAYGROUND_SURFACES: Record
badge: {icon: ShieldIcon},
body: (
<>
- First detected in commit{' '}
-
+ First detected in commit
>
),
},
@@ -119,7 +117,7 @@ export const PLAYGROUND_SURFACES: Record
badge: {icon: ShieldCheckIcon, variant: 'done'},
body: (
<>
- Fixed in branchmain{' '}
+ Fixed in branchmain{' '}
>
),
@@ -140,8 +138,7 @@ export const PLAYGROUND_SURFACES: Record
<>
{'closed this as '}
- false positive{' '}
-
+ false positive
>
),
},
@@ -258,7 +255,7 @@ export const PLAYGROUND_SURFACES: Record
<>
{'closed this as '}
- revoked
+ revoked
Rotated the leaked token and confirmed the provider revoked it.
@@ -392,8 +389,7 @@ export const PLAYGROUND_SURFACES: Record
<>
{'dismissed this as '}
- risk is tolerable{' '}
-
+ risk is tolerableOnly reachable from a dev-only script we do not ship.
>
),
@@ -458,8 +454,7 @@ export const PLAYGROUND_SURFACES: Record
body: (
<>
{'Appeared in branch '}
- feature-branch{' '}
-
+ feature-branch
>
),
},
@@ -546,9 +541,15 @@ export const PLAYGROUND_SURFACES: Record
issue: {
label: 'Issues',
ariaLabel: 'Issue timeline',
+ // Per-category `description` strings below are REPRESENTATIVE one-liners, surfaced by
+ // the Viewing menu via `ActionList.Description`. They are NOT authoritative: the
+ // canonical per-category descriptions (and cross-surface consistency) belong to
+ // `@github-ui/timeline-taxonomy` (event-categories.ts). Primer renders the slot but
+ // does not own the category semantics.
categories: {
status: {
label: 'Status',
+ description: 'Opened, closed, and reopened lifecycle events.',
events: [
{
type: 'closed',
@@ -587,6 +588,7 @@ export const PLAYGROUND_SURFACES: Record
},
references: {
label: 'References',
+ description: 'Cross-links to pull requests, commits, and other issues.',
events: [
{
type: 'connected',
@@ -630,6 +632,7 @@ export const PLAYGROUND_SURFACES: Record
},
moderation: {
label: 'Moderation',
+ description: 'Blocks, hides, and other moderation actions.',
events: [
{
type: 'user_blocked',
@@ -668,6 +671,7 @@ export const PLAYGROUND_SURFACES: Record
},
metadata: {
label: 'Metadata',
+ description: 'Labels, milestones, and other bookkeeping changes.',
events: [
{
type: 'labeled',
diff --git a/packages/react/src/Timeline/internal/timelineStoryHelpers.module.css b/packages/react/src/Timeline/internal/timelineStoryHelpers.module.css
index 4783e9d3b8b..4b5aca62624 100644
--- a/packages/react/src/Timeline/internal/timelineStoryHelpers.module.css
+++ b/packages/react/src/Timeline/internal/timelineStoryHelpers.module.css
@@ -51,6 +51,13 @@
margin-right: var(--base-size-4);
}
+/* Inline semibold emphasis in event bodies. Timeline.Body is muted, so the explicit
+ default color lifts the emphasized phrase out of the muted run. */
+.Strong {
+ font-weight: var(--base-text-weight-semibold);
+ color: var(--fgColor-default);
+}
+
/* Trailing "bot" Label spacing in UserActor. */
.BotLabel {
margin-left: var(--base-size-4);
diff --git a/packages/react/src/Timeline/internal/timelineStoryHelpers.tsx b/packages/react/src/Timeline/internal/timelineStoryHelpers.tsx
index f50d805ca7b..4593d257dfc 100644
--- a/packages/react/src/Timeline/internal/timelineStoryHelpers.tsx
+++ b/packages/react/src/Timeline/internal/timelineStoryHelpers.tsx
@@ -62,6 +62,15 @@ export const BoldLink = ({className, ...props}: React.ComponentProps
)
+/**
+ * Inline semibold emphasis for event-body text. `Timeline.Body` renders muted, so this
+ * also restores the default foreground color to lift the emphasized phrase out of the
+ * muted run (the two-tone event sentences live-GitHub uses).
+ */
+export const Strong = ({className, ...props}: React.ComponentProps<'span'>) => (
+
+)
+
// TODO(github/primer#6827): remove when Primer ships an inline (in-text) avatar treatment
export const InlineAvatar = ({className, size = 20, alt = '', ...props}: React.ComponentProps) => (
From c05219f052b6c99831fc5ad170e356a957391591 Mon Sep 17 00:00:00 2001
From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com>
Date: Fri, 21 Aug 2026 11:57:30 -0700
Subject: [PATCH 3/3] Keep Timeline Viewing menu open across category toggles
Call event.preventDefault() in the multi-select ViewingFilterMenu onSelect so ActionMenu keeps its overlay open across toggles for mouse and keyboard. The prior handler let afterSelect close the menu on each item-select, contradicting the stay-open doc comment.
---
.../src/Timeline/Timeline.filter.features.stories.tsx | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/packages/react/src/Timeline/Timeline.filter.features.stories.tsx b/packages/react/src/Timeline/Timeline.filter.features.stories.tsx
index f8e840d180c..ccbc92ec7fd 100644
--- a/packages/react/src/Timeline/Timeline.filter.features.stories.tsx
+++ b/packages/react/src/Timeline/Timeline.filter.features.stories.tsx
@@ -77,9 +77,14 @@ function ViewingFilterMenu({
+ onSelect={event => {
+ // Keep the multi-select Viewing menu open across toggles: ActionMenu
+ // closes its overlay on item-select unless the handler prevents the
+ // default. This works for mouse and keyboard (the Space path resets
+ // `defaultPrevented` before calling this handler).
+ event.preventDefault()
onSelectedChange(isSelected ? selected.filter(id => id !== option.id) : [...selected, option.id])
- }
+ }}
>
{option.label}
{option.description ? (