Skip to content

Commit c63da2b

Browse files
Merge /wip into /roadmap
One page now shows the early-access feature board plus a compact grid of what every team is working on this quarter. Team cards open the shared drawer with the team's quarterly objectives and its roadmap features; ?team= deep links (including legacy name-form links from product pages) are honored. /wip is deleted and redirected to /roadmap. Generated-By: PostHog Code Task-Id: 9ecd4529-67fb-4091-9d52-2f321cccc016
1 parent f11e18f commit c63da2b

14 files changed

Lines changed: 583 additions & 421 deletions

File tree

contents/handbook/company/goal-setting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ If you aren't on a product team, replace 'product' with the equivalent 'thing' o
8484

8585
When a team has set their quarterly goals it is the responsibility of the [team lead](/handbook/company/small-teams#what-is-the-role-of-the-team-lead) to document the goals on their team page, [publicly](/handbook/values). This enables teams to see what each other are working on, helps us hold teams accountable to their goals, and creates a shared sense of urgency and direction.
8686

87-
You can easily see what goals teams have set on the [WIP](/wip) page, which pulls all goals from the respective team pages.
87+
You can easily see what goals teams have set on the [roadmap](/roadmap) page, which pulls all goals from the respective team pages.
8888

8989
Teams can choose to document their goals publicly in a number of formats, but below is a useful template for getting started. Individuals may also choose to create planning issues to track their work in greater detail.
9090

contents/handbook/engineering/posthog-com/changelog.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The [changelog](/changelog) publishing process is run by the <SmallTeam slug="wi
66

77
Changelog entries can be created in two ways:
88

9-
1. Change the status of an existing [roadmap](/roadmap) or [WIP](/wip) entry to "Complete"
9+
1. Change the status of an existing [roadmap](/roadmap) entry to "Complete"
1010
2. Create a new changelog entry on the [changelog](/changelog) page.
1111

1212
Both options are available when you're signed into posthog.com as a moderator.

src/components/Korean/KoreanTaskBarMenu/menuData.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -532,11 +532,6 @@ export function useMenuData(): MenuType[] {
532532
label: 'Roadmap',
533533
link: '/roadmap',
534534
},
535-
{
536-
type: 'item',
537-
label: 'WIP',
538-
link: '/wip',
539-
},
540535
{
541536
type: 'item',
542537
label: 'Changelog',

src/components/Roadmap/EarlyAccessFeaturesSection.tsx

Lines changed: 229 additions & 44 deletions
Large diffs are not rendered by default.

src/components/Roadmap/README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# Roadmap board
22

3-
`EarlyAccessFeaturesSection` powers `/roadmap`. It presents the same early-access data and enrollment flows as the PostHog app in a compact, changelog-style board.
3+
`EarlyAccessFeaturesSection` powers `/roadmap`. It presents the same early-access data and enrollment flows as the PostHog app in a compact, changelog-style board, followed by a quarter grid showing what every team is working on (the former `/wip` page, which now redirects here).
44

55
## Data ownership
66

77
- `useEarlyAccessFeatures` owns the feature data. It starts with Gatsby's build-time `EarlyAccessFeature` nodes, then revalidates with PostHog JS in the browser.
88
- The hook supplies the feature stage, title, description, documentation URL, flag key, creation date, waitlist count, and waitlist survey payload.
99
- `useFeatureOwnership` supplies the shared feature-to-small-team map. `roadmapTeamOverrides.ts` is the curated flag-key override and takes precedence over that map.
10-
- `allSqueakTeam` supplies display names, mini crests, and member profiles. Features without a match remain visible and can be filtered as `Unassigned`.
10+
- `allSqueakTeam` supplies display names, mini crests, crest presence, member profiles, and quarterly objectives. Features without a match remain visible and can be filtered as `Unassigned`.
11+
- Objectives are freeform MDX at `contents/teams/{slug}/objectives.mdx`, resolved onto `SqueakTeam.objectives` by the schema customization in `gatsby/createSchemaCustomization.ts`. `TeamObjectives.tsx` owns the current-quarter parsing: `getCurrentQuarter` derives the quarter from today's date, and `QuarterObjectives` renders the MDX then hides everything outside the heading matching `Q{quarter} {year}`, collapsing "recap" sections into a native `<details>`.
1112

1213
Do not duplicate this data in the component or add hard-coded feature cards. Update the source early-access feature, shared ownership data, or the roadmap override map instead.
1314

@@ -26,11 +27,15 @@ Cards intentionally contain only:
2627

2728
`New` means the feature was created within the configured new-feature window. `Popular` is assigned to the top features with a positive build-time waitlist count. Badges never change the card border; the selected card uses the standard accent background.
2829

29-
Every lane ends with the same dashed `Your idea here` card. It opens the shared overlay drawer containing the roadmap pitch form; the form is not duplicated per lane.
30+
The Concept lane ends with the dashed `Your idea here` card. It opens the shared overlay drawer containing the roadmap pitch form; the form is not duplicated per lane.
31+
32+
## Team quarter grid
33+
34+
Below the board, `TeamQuarterSection` renders one compact card per team: mini crest (with an icon fallback), team name, and a muted count of that team's roadmap features when it has any. The team set mirrors the old `/wip` query — crested teams, excluding Hedgehogs — so teams without an objectives file still get a card; their drawer shows the "hasn't set goals yet" state. Cards are alphabetical and are buttons that open the shared drawer, not links. The grid uses container-query columns and never scrolls itself; the Editor window keeps the page's only vertical scrollbar. When filters produce zero cards the grid shows a single muted "No teams match" line (unlike lanes, an empty grid under a heading reads as broken). The board area behaves the same way in reverse: while features load or are unavailable, a single muted line appears in its place and the team grid stays fully usable.
3035

3136
## Filtering
3237

33-
One search query is applied before grouping into lanes. It matches the title, full description, flag key, and owning team name. Search is cleared directly from the search field. The team selector is applied at the same time, includes an `Unassigned` option when necessary, and resets through its `All teams` option. Neither control adds a redundant filter chip below the toolbar. The toolbar uses the same primary surface as each lane header, and its rounded search and team controls share one height. Counts always reflect both filters. Empty lanes stay visually empty rather than adding a redundant no-results message; their zero count already communicates the state.
38+
One search query is applied before grouping into lanes. It matches the title, full description, flag key, and owning team name — and, for the team grid, the team name. Search is cleared directly from the search field. The team selector is applied at the same time and filters both surfaces: it lists every grid team (with its feature count, zero allowed) plus any feature-owning slugs without a grid card, includes an `Unassigned` option when necessary (which matches no grid card), and resets through its `All teams` option. Neither control adds a redundant filter chip below the toolbar. The toolbar uses the same primary surface as each lane header, and its rounded search and team controls share one height. Counts always reflect both filters. Empty lanes stay visually empty rather than adding a redundant no-results message; their zero count already communicates the state.
3439

3540
Featured ordering is preserved within each lane: new cards lead, and joinable alpha/concept items lead their stage. There are no stage or sorting controls because the lanes themselves communicate stage.
3641

@@ -56,9 +61,9 @@ The drawer animates only when it opens from a closed state or closes. Switching
5661

5762
The drawer is non-modal. Clicking another feature or idea card replaces its content without closing the shell or replaying the animation. Clicking elsewhere inside the roadmap's Editor surface closes it; clicks within the drawer itself do not. Escape remains disabled, and the standard `OSButton` window close control always dismisses it. This behavior uses one Editor-scoped click listener plus `data-roadmap-item` and `data-roadmap-drawer` markers—do not add a scrim or restore modal outside-click handling.
5863

59-
The drawer contains the full description, documentation link, linked small team, complete avatar roster with tooltips, and the stage-specific action.
64+
For a feature, the drawer contains the full description, documentation link, linked small team, complete avatar roster with tooltips, and the stage-specific action. For a team (`TeamPanel`), it contains the team's current-quarter objectives and its roadmap features. The two hop symmetrically without a back stack: feature rows inside the team panel swap the drawer to that feature (showing a stage chip instead of the redundant team line), and the feature panel's "View team goals" button swaps back to the team — offered only when the owning team has a grid card.
6065

61-
The canonical URL is `/roadmap?feature=<flagKey>`. Opening a card adds only `feature` with a history replacement, and closing removes only that parameter the same way. This keeps visible URLs shareable without turning browser Back into another drawer-dismiss control. Invalid values leave the board usable and are removed safely after feature data loads.
66+
The canonical URLs are `/roadmap?feature=<flagKey>` and `/roadmap?team=<slug>`, and the URL carries at most one drawer param — setting one clears the other, and `feature` wins if both arrive. Opening a card adds its param with a history replacement, and closing removes it the same way. This keeps visible URLs shareable without turning browser Back into another drawer-dismiss control. Invalid values leave the board usable and are removed safely after feature data loads. Legacy inbound links pass a team name (`/roadmap?team=Session%20Replay`, from product pages): a case-insensitive name match normalizes the URL to the slug, sets the team filter to that team, and opens its drawer; unresolvable values are stripped.
6267

6368
Legacy `/roadmap#<flagKey>` URLs are supported. A valid hash is normalized to the canonical query URL with a history replacement.
6469

@@ -69,6 +74,6 @@ The drawer's copy control always copies the canonical URL, without unrelated fil
6974
- **Beta:** a full-width external `Enable` button opens the matching feature preview in the PostHog app. It uses the same medium primary `OSButton` sizing and full-width contract as the notification form. Do not enroll the anonymous website visitor locally.
7075
- **Alpha:** `SurveySignup` records the linked waitlist survey response without a concept enrollment event.
7176
- **Concept:** `SurveySignup` records the linked waitlist survey and receives the flag key, preserving the existing concept-stage enrollment behavior.
72-
- **Pitch an idea:** the dashed card at the bottom of every lane opens the shared drawer and records the existing roadmap concept-pitch survey. The idea and optional email fields use the same stacked, bold-label form treatment. Its submit action uses the same medium primary full-width button treatment as the feature actions.
77+
- **Pitch an idea:** the dashed card at the bottom of the Concept lane opens the shared drawer and records the existing roadmap concept-pitch survey. The idea and optional email fields use the same stacked, bold-label form treatment. Its submit action uses the same medium primary full-width button treatment as the feature actions.
7378

7479
`SurveySignup` owns returning-user state in local storage. Keep these flows delegated to the shared components so analytics and persistence remain consistent elsewhere on the site.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import React, { useEffect, useRef } from 'react'
2+
import { MDXProvider } from '@mdx-js/react'
3+
import { MDXRenderer } from 'gatsby-plugin-mdx'
4+
import TeamMemberComponent, { FutureTeamMember } from 'components/TeamMember'
5+
import SmallTeam from 'components/SmallTeam'
6+
7+
const TypedMDXProvider = MDXProvider as React.ComponentType<{
8+
components: Record<string, React.ComponentType<any>> // eslint-disable-line @typescript-eslint/no-explicit-any
9+
children: React.ReactNode
10+
}>
11+
12+
export function getCurrentQuarter(): { quarter: number; year: number } {
13+
const now = new Date()
14+
const quarter = Math.floor(now.getMonth() / 3) + 1
15+
return { quarter, year: now.getFullYear() }
16+
}
17+
18+
/**
19+
* Renders a team's objectives.mdx body scoped to the current quarter. The MDX files are
20+
* freeform prose with quarter-labeled headings (e.g. "Q3 2026 Objectives", "Q2 2026 Recap"),
21+
* so after render this hides everything outside the section under the heading matching
22+
* `Q{quarter} {year}` and collapses any "recap" section into a native <details>.
23+
*/
24+
export function QuarterObjectives({
25+
body,
26+
quarter,
27+
year,
28+
}: {
29+
body: string
30+
quarter: number
31+
year: number
32+
}): JSX.Element {
33+
const ref = useRef<HTMLDivElement>(null)
34+
35+
useEffect(() => {
36+
if (!ref.current) return
37+
const container = ref.current
38+
const headings = container.querySelectorAll('h1, h2, h3')
39+
const qRegex = new RegExp(`Q${quarter}\\s+${year}`, 'i')
40+
const recapRegex = /recap/i
41+
42+
let currentQHeading: Element | null = null
43+
let currentQLevel = 0
44+
45+
for (const h of Array.from(headings)) {
46+
const text = h.textContent || ''
47+
if (qRegex.test(text) && !recapRegex.test(text)) {
48+
currentQHeading = h
49+
currentQLevel = parseInt(h.tagName[1])
50+
break
51+
}
52+
}
53+
54+
if (!currentQHeading) return
55+
56+
// Hide the quarter heading itself (the surrounding UI already shows the quarter)
57+
const qHeadEl = currentQHeading as HTMLElement
58+
qHeadEl.style.display = 'none'
59+
60+
// Hide everything before the current quarter heading
61+
let el = container.firstElementChild
62+
while (el && el !== currentQHeading) {
63+
const next = el.nextElementSibling
64+
const elHtml = el as HTMLElement
65+
elHtml.style.display = 'none'
66+
el = next
67+
}
68+
69+
// Process elements after the current quarter section
70+
let sibling = currentQHeading.nextElementSibling
71+
let pastCurrentSection = false
72+
73+
while (sibling) {
74+
const next = sibling.nextElementSibling
75+
76+
if (sibling.matches('h1, h2, h3') && parseInt(sibling.tagName[1]) <= currentQLevel) {
77+
pastCurrentSection = true
78+
const text = sibling.textContent || ''
79+
80+
if (recapRegex.test(text)) {
81+
// Wrap recap section in a collapsible <details>
82+
const details = document.createElement('details')
83+
details.className = 'my-3 border border-primary rounded'
84+
const summary = document.createElement('summary')
85+
summary.className =
86+
'cursor-pointer px-3 py-2 text-sm font-semibold text-secondary hover:text-primary list-none flex items-center gap-1.5'
87+
summary.textContent = text
88+
details.appendChild(summary)
89+
90+
const wrapper = document.createElement('div')
91+
wrapper.className = 'px-3 pb-3'
92+
93+
const sibHtml = sibling as HTMLElement
94+
sibHtml.style.display = 'none'
95+
96+
let recapEl = next
97+
while (recapEl) {
98+
if (recapEl.matches('h1, h2, h3') && parseInt(recapEl.tagName[1]) <= currentQLevel) {
99+
break
100+
}
101+
const recapNext = recapEl.nextElementSibling
102+
wrapper.appendChild(recapEl)
103+
recapEl = recapNext
104+
}
105+
106+
details.appendChild(wrapper)
107+
sibling.parentNode?.insertBefore(details, sibling.nextSibling)
108+
sibling = recapEl
109+
continue
110+
}
111+
}
112+
113+
if (pastCurrentSection) {
114+
const pastSibHtml = sibling as HTMLElement
115+
pastSibHtml.style.display = 'none'
116+
}
117+
118+
sibling = next
119+
}
120+
}, [body, quarter, year])
121+
122+
return (
123+
<div ref={ref} className="prose prose-sm max-w-none dark:prose-invert">
124+
<TypedMDXProvider
125+
components={{
126+
TeamMember: TeamMemberComponent,
127+
FutureTeamMember,
128+
SmallTeam,
129+
}}
130+
>
131+
<MDXRenderer>{body}</MDXRenderer>
132+
</TypedMDXProvider>
133+
</div>
134+
)
135+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React from 'react'
2+
import { GatsbyImage, getImage } from 'gatsby-plugin-image'
3+
import Link from 'components/Link'
4+
import ScrollArea from 'components/RadixUI/ScrollArea'
5+
import { EarlyAccessFeature } from 'hooks/useEarlyAccessFeatures'
6+
import { QuarterObjectives } from './TeamObjectives'
7+
8+
export interface TeamPanelTeam {
9+
slug: string
10+
name: string
11+
miniCrest?: Parameters<typeof getImage>[0]
12+
}
13+
14+
/**
15+
* Drawer content for a team: its current-quarter objectives plus its roadmap features.
16+
* Feature rows are rendered by the caller (via renderFeature) so this file doesn't import
17+
* the board's card components back out of the orchestrator.
18+
*/
19+
const TeamPanel = ({
20+
team,
21+
objectivesBody,
22+
quarter,
23+
year,
24+
features,
25+
renderFeature,
26+
}: {
27+
team: TeamPanelTeam
28+
objectivesBody?: string
29+
quarter: number
30+
year: number
31+
features: EarlyAccessFeature[]
32+
renderFeature: (feature: EarlyAccessFeature) => React.ReactNode
33+
}): JSX.Element => {
34+
const crest = team.miniCrest ? getImage(team.miniCrest) : undefined
35+
36+
return (
37+
<div data-scheme="primary" className="flex h-full min-h-0 flex-col bg-primary">
38+
<header className="shrink-0 border-b border-primary px-4 py-4 pr-14">
39+
<div className="flex items-center gap-3">
40+
{crest && (
41+
<GatsbyImage image={crest} alt={`${team.name} team mini crest`} className="size-10 shrink-0" />
42+
)}
43+
<div className="min-w-0">
44+
<h2 className="m-0 text-2xl leading-tight">{team.name} Team</h2>
45+
<Link
46+
to={`/teams/${team.slug}`}
47+
state={{ newWindow: true }}
48+
className="text-sm font-semibold text-red dark:text-yellow"
49+
>
50+
Visit team page
51+
</Link>
52+
</div>
53+
</div>
54+
</header>
55+
56+
<ScrollArea className="min-h-0 flex-1">
57+
<div className="flex flex-col gap-6 p-4">
58+
<div>
59+
<h3 className="mb-2 mt-0 text-sm">
60+
This quarter (Q{quarter} {year})
61+
</h3>
62+
{objectivesBody ? (
63+
<QuarterObjectives body={objectivesBody} quarter={quarter} year={year} />
64+
) : (
65+
<p className="m-0 text-sm italic text-secondary">This team hasn't set goals yet.</p>
66+
)}
67+
</div>
68+
{features.length > 0 && (
69+
<div>
70+
<h3 className="mb-2 mt-0 text-sm">On the roadmap</h3>
71+
<ul className="m-0 flex list-none flex-col gap-2 p-0">
72+
{features.map((feature) => (
73+
<li key={feature.flagKey} className="m-0 list-none p-0">
74+
{renderFeature(feature)}
75+
</li>
76+
))}
77+
</ul>
78+
</div>
79+
)}
80+
</div>
81+
</ScrollArea>
82+
</div>
83+
)
84+
}
85+
86+
export default TeamPanel

0 commit comments

Comments
 (0)