1
0
mirror of synced 2025-12-19 18:10:59 -05:00

journey learning tracks UI (#57538)

This commit is contained in:
Robert Sese
2025-09-23 13:18:42 -05:00
committed by GitHub
parent dc2a9ed7f5
commit 0a3d8ccdab
3 changed files with 388 additions and 1 deletions

View File

@@ -1,16 +1,105 @@
import { DefaultLayout } from '@/frame/components/DefaultLayout' import { DefaultLayout } from '@/frame/components/DefaultLayout'
import { useLandingContext } from '@/landings/context/LandingContext' import { useLandingContext } from '@/landings/context/LandingContext'
import { LandingHero } from '@/landings/components/shared/LandingHero' import { LandingHero } from '@/landings/components/shared/LandingHero'
import { JourneyLearningTracks } from './JourneyLearningTracks'
export type JourneyLearningTrack = {
id: string
title: string
description: string
trackName: string
trackProduct: string
guides?: Array<{
href: string
title: string
}>
}
export const JourneyLanding = () => { export const JourneyLanding = () => {
const { title, intro, heroImage, introLinks } = useLandingContext() const { title, intro, heroImage, introLinks } = useLandingContext()
// Temp until we hookup real data
const stubLearningTracks: JourneyLearningTrack[] = [
{
id: 'admin:get_started_with_your_enterprise_account',
title: 'Get started with your enterprise account',
description:
'Set up your enterprise account and configure initial settings for your organization.',
trackName: 'get_started_with_your_enterprise_account',
trackProduct: 'admin',
guides: [
{
href: '/admin/overview/about-enterprise-accounts?learn=get_started_with_your_enterprise_account&learnProduct=admin',
title: 'About enterprise accounts',
},
{
href: '/admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/inviting-people-to-manage-your-enterprise?learn=get_started_with_your_enterprise_account&learnProduct=admin',
title: 'Inviting people to manage your enterprise',
},
{
href: '/admin/policies/enforcing-policies-for-your-enterprise/about-enterprise-policies?learn=get_started_with_your_enterprise_account&learnProduct=admin',
title: 'About enterprise policies',
},
],
},
{
id: 'admin:adopting_github_actions_for_your_enterprise_ghec',
title: 'Adopt GitHub Actions for your enterprise',
description:
'Learn how to plan and implement a rollout of GitHub Actions in your enterprise.',
trackName: 'adopting_github_actions_for_your_enterprise_ghec',
trackProduct: 'admin',
guides: [
{
href: '/admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/about-github-actions-for-enterprises?learn=adopting_github_actions_for_your_enterprise_ghec&learnProduct=admin',
title: 'About GitHub Actions for enterprises',
},
{
href: '/actions/get-started/understand-github-actions?learn=adopting_github_actions_for_your_enterprise_ghec&learnProduct=admin',
title: 'Understanding GitHub Actions',
},
{
href: '/admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/introducing-github-actions-to-your-enterprise?learn=adopting_github_actions_for_your_enterprise_ghec&learnProduct=admin',
title: 'Introducing GitHub Actions to your enterprise',
},
{
href: '/admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/migrating-your-enterprise-to-github-actions?learn=adopting_github_actions_for_your_enterprise_ghec&learnProduct=admin',
title: 'Migrating your enterprise to GitHub Actions',
},
],
},
{
id: 'actions:continuous-integration',
title: 'Continuous integration with GitHub Actions',
description:
'Set up automated testing and building for your projects using GitHub Actions workflows.',
trackName: 'continuous-integration',
trackProduct: 'actions',
guides: [
{
href: '/actions/automating-builds-and-tests/about-continuous-integration?learn=continuous-integration&learnProduct=actions',
title: 'About continuous integration',
},
{
href: '/actions/automating-builds-and-tests/building-and-testing-nodejs?learn=continuous-integration&learnProduct=actions',
title: 'Building and testing Node.js',
},
{
href: '/actions/automating-builds-and-tests/building-and-testing-python?learn=continuous-integration&learnProduct=actions',
title: 'Building and testing Python',
},
],
},
]
return ( return (
<DefaultLayout> <DefaultLayout>
<div> <div>
<LandingHero title={title} intro={intro} heroImage={heroImage} introLinks={introLinks} /> <LandingHero title={title} intro={intro} heroImage={heroImage} introLinks={introLinks} />
<div>TODO</div> <div className="container-xl px-3 px-md-6 mt-6 mb-4">
<JourneyLearningTracks tracks={stubLearningTracks} />
</div>
</div> </div>
</DefaultLayout> </DefaultLayout>
) )

View File

@@ -0,0 +1,213 @@
.learningTracks {
border: 1px solid var(--borderColor-default, var(--color-border-default, #d1d9e0));
border-radius: 12px;
padding: 1.5rem;
padding-bottom: .75rem;
margin-bottom: 1rem;
margin-left: 1rem;
box-shadow:
0px 1px 3px 0px rgba(31, 35, 40, 0.08),
0px 1px 0px 0px rgba(31, 35, 40, 0.06);
position: relative;
z-index: 1;
background-color: var(--bgColor-default, var(--color-canvas-default, #ffffff));
}
.trackHeader {
margin: 0 0 0.5rem 0;
padding-right: 3rem;
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
display: flex;
align-items: center;
gap: 0.5rem;
}
.anchorLink {
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
text-decoration: none;
}
.trackDescription {
margin: 0 0 1rem 0;
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
}
.expandButton {
top: 0;
right: 0;
}
.trackGuides {
margin-top: 1rem;
margin-bottom: 1rem;
padding-left: 0;
list-style: none;
counter-reset: guide-counter;
}
.trackGuides li {
margin-bottom: 0.75rem;
padding-left: 2.5rem;
position: relative;
counter-increment: guide-counter;
}
.trackGuides li::before {
content: counter(guide-counter);
position: absolute;
left: 0;
top: -0.125rem;
width: 1.5rem;
height: 1.5rem;
background-color: transparent;
border: 1px solid var(--borderColor-default, var(--color-border-default, #d0d7de));
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.70rem;
font-weight: 600;
line-height: 1;
}
.guideLink {
color: var(--fgColor-accent, var(--color-accent-fg, #0969da));
text-decoration: none;
}
/* Hide only the timeline line that extends below the last badge, preserve everything else */
.timelineContainer :global(.Timeline-Item:last-child::before) {
background: linear-gradient(to bottom, var(--borderColor-default, #d1d9e0) 0%, var(--borderColor-default, #d1d9e0) 30%, transparent 30%, transparent 100%) !important;
}
.timelineBadge {
background-color: var(--color-canvas-subtle, #f6f8fa) !important;
border: 1px solid var(--borderColor-default, var(--color-border-default, #d1d9e0)) !important;
}
/* Fix entire timeline component overlapping header */
.timelineContainer {
z-index: 0 !important;
position: relative;
}
.timelineContainer :global(.Timeline) {
z-index: 0 !important;
}
.timelineThinLine :global(.Timeline-Item::before) {
width: 1px !important;
}
/* Mobile-first: custom stacked layout */
.mobileLayout {
display: block;
position: relative;
z-index: 0;
}
.mobileItem {
margin-bottom: 2rem;
text-align: center; /* Only for centering the badge */
position: relative;
}
.mobileBadge {
width: 2rem;
height: 2rem;
background-color: var(--color-canvas-subtle, #f6f8fa);
color: var(--fgColor-muted, var(--color-fg-muted));
border: 1px solid var(--borderColor-default, var(--color-border-default, #d1d9e0));
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-bottom: 0.5rem;
position: relative;
z-index: 2;
}
/* Add connecting line from badge downward */
.mobileBadge::after {
content: '';
position: absolute;
left: 50%;
top: 100%;
width: 1px;
height: 2.5rem;
background-color: var(--borderColor-default, var(--color-border-default, #d1d9e0));
transform: translateX(-50%);
z-index: 1;
}
/* Add connecting line above badge (except first item) */
.mobileItem:not(:first-child) .mobileBadge::before {
content: '';
position: absolute;
left: 50%;
bottom: 100%;
width: 1px;
height: 2.5rem;
background-color: var(--borderColor-default, var(--color-border-default, #d1d9e0));
transform: translateX(-50%);
z-index: 1;
}
.mobileConnector {
width: 1px;
height: 1rem;
background-color: var(--borderColor-default, var(--color-border-default, #d1d9e0));
margin: 0 auto;
}
.mobileConnector:last-child {
display: none; /* Hide connector after last item */
}
.mobileItem .mobileTile {
border: 1px solid var(--borderColor-default, var(--color-border-default, #d1d9e0));
border-radius: 12px;
padding: 1rem;
margin-top: 0.5rem;
text-align: left;
box-shadow:
0px 1px 3px 0px rgba(31, 35, 40, 0.08),
0px 1px 0px 0px rgba(31, 35, 40, 0.06);
position: relative;
z-index: 3;
background-color: var(--bgColor-default, var(--color-canvas-default, #ffffff));
}
/* Desktop: show Timeline component */
@media (min-width: 768px) {
.mobileLayout {
display: none;
}
.timelineContainer {
display: block;
}
}
/* Mobile: hide Timeline component */
@media (max-width: 767px) {
.timelineContainer {
display: none;
}
}
/* Mobile: stack h3 and Token vertically when mobile layout is active */
@media (max-width: 767px) {
.trackHeader {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.trackHeader h3 {
margin-bottom: .50rem;
}
}

View File

@@ -0,0 +1,85 @@
/* filepath: /workspaces/docs-internal/src/landings/components/journey/JourneyLearningTracks.tsx */
import { ChevronDownIcon, ChevronUpIcon } from '@primer/octicons-react'
import { Button, Details, Timeline, Token, useDetails } from '@primer/react'
import type { JourneyLearningTrack } from './JourneyLanding'
import styles from './JourneyLearningTracks.module.css'
type JourneyLearningTracksProps = {
tracks: JourneyLearningTrack[]
}
export const JourneyLearningTracks = ({ tracks }: JourneyLearningTracksProps) => {
if (!tracks || tracks.length === 0) {
return null
}
const renderTrackContent = (track: JourneyLearningTrack, trackIndex: number) => {
const { getDetailsProps, open } = useDetails({})
return (
<>
<div className={styles.trackHeader}>
<h3 className="h4 text-bold">{track.title}</h3>
<Token text={`${track.guides?.length || 0} articles`} />
</div>
<p className={styles.trackDescription}>{track.description}</p>
<Details {...getDetailsProps()}>
<Button
as="summary"
variant="invisible"
className={`position-absolute ${styles.expandButton}`}
aria-label={
open
? `Collapse article list ${trackIndex + 1}`
: `Expand article list ${trackIndex + 1}`
}
>
{open ? <ChevronUpIcon /> : <ChevronDownIcon />}
</Button>
<ol className={styles.trackGuides}>
{(track.guides || []).map((guide) => (
<li key={guide.title}>
<a href={guide.href} className={`text-semibold ${styles.guideLink}`}>
{guide.title}
</a>
</li>
))}
</ol>
</Details>
</>
)
}
return (
<>
{/* Desktop: Timeline component */}
<div className={styles.timelineContainer}>
<Timeline clipSidebar className={styles.timelineThinLine}>
{tracks.map((track, trackIndex) => {
return (
<Timeline.Item key={track.id}>
<Timeline.Badge className={styles.timelineBadge}>{trackIndex + 1}</Timeline.Badge>
<Timeline.Body className={styles.learningTracks}>
<div className="position-relative">{renderTrackContent(track, trackIndex)}</div>
</Timeline.Body>
</Timeline.Item>
)
})}
</Timeline>
</div>
{/* Mobile: Custom stacked layout */}
<div className={styles.mobileLayout}>
{tracks.map((track, trackIndex) => (
<div key={track.id} className={styles.mobileItem}>
<div className={styles.mobileBadge}>{trackIndex + 1}</div>
<div className={styles.mobileTile}>
<div className="position-relative">{renderTrackContent(track, trackIndex)}</div>
</div>
{trackIndex < tracks.length - 1 && <div className={styles.mobileConnector} />}
</div>
))}
</div>
</>
)
}