11---
2- import type { ApiProject } from ' ../types/roadmap'
2+ import type { ApiProject , BoardTeam } from ' ../types/roadmap'
33
44interface Props {
55 projects? : ApiProject []
6+ teams? : BoardTeam []
67}
78
8- const { projects = [] }: Props = Astro .props
9+ const { projects = [], teams = [] }: Props = Astro .props
910
1011// ─── Icon resolution ─────────────────────────────────────────────────────────
1112
@@ -200,8 +201,66 @@ const nowYear = now.getUTCFullYear()
200201const nowMonth = now .getUTCMonth ()
201202const nowLeft = pctLeft (now ).toFixed (2 )
202203
203- // Project rows start at grid row 3 (rows 1–2 are the header)
204- const PROJECT_ROW_OFFSET = 3
204+ // ─── Team grouping ───────────────────────────────────────────────────────────
205+
206+ const teamMap = new Map (teams .map ((t ) => [t .id , t ]))
207+
208+ // Root teams: not referenced as a child by any other team
209+ const allChildIds = new Set (teams .flatMap ((t ) => t .childrenIds ))
210+ const rootTeams = teams .filter ((t ) => ! allChildIds .has (t .id ))
211+
212+ // Index projects by their direct team ID
213+ const projectsByTeamId = new Map <string , ApiProject []>()
214+ for (const proj of projects ) {
215+ if (! proj .team ) continue
216+ const tid = proj .team .id
217+ if (! projectsByTeamId .has (tid )) projectsByTeamId .set (tid , [])
218+ projectsByTeamId .get (tid )! .push (proj )
219+ }
220+
221+ // Collect all projects for a root team (own + children), sorted by sortOrder
222+ function collectTeamProjects(teamId : string ): ApiProject [] {
223+ const team = teamMap .get (teamId )
224+ if (! team ) return []
225+ const own = projectsByTeamId .get (teamId ) ?? []
226+ const fromChildren = team .childrenIds .flatMap (
227+ (cid ) => projectsByTeamId .get (cid ) ?? []
228+ )
229+ return [... own , ... fromChildren ].sort ((a , b ) => a .sortOrder - b .sortOrder )
230+ }
231+
232+ // Build flat grid item list with pre-assigned row numbers
233+ type GridItem =
234+ | { type: ' team-header' ; team: BoardTeam ; row: number ; teamColor: string }
235+ | { type: ' project' ; project: ApiProject ; row: number ; teamColor: string }
236+
237+ const gridItems: GridItem [] = []
238+ let currentRow = 3 // rows 1–2 are quarter/month headers
239+
240+ for (const rootTeam of rootTeams ) {
241+ const teamProjects = collectTeamProjects (rootTeam .id )
242+ if (teamProjects .length === 0 ) continue
243+ const teamColor = rootTeam .color ?? ' #888888'
244+ gridItems .push ({ type: ' team-header' , team: rootTeam , row: currentRow , teamColor })
245+ currentRow ++
246+ for (const proj of teamProjects ) {
247+ gridItems .push ({ type: ' project' , project: proj , row: currentRow , teamColor })
248+ currentRow ++
249+ }
250+ }
251+
252+ // Append uncategorised projects (no team, or team not in the hierarchy)
253+ const assignedIds = new Set (
254+ gridItems .flatMap ((i ) => (i .type === ' project' ? [i .project .id ] : []))
255+ )
256+ const uncategorised = projects
257+ .filter ((p ) => ! assignedIds .has (p .id ))
258+ .sort ((a , b ) => a .sortOrder - b .sortOrder )
259+ for (const proj of uncategorised ) {
260+ const teamColor = proj .team ?.color ?? ' #888888'
261+ gridItems .push ({ type: ' project' , project: proj , row: currentRow , teamColor })
262+ currentRow ++
263+ }
205264---
206265
207266<div class =" board-outer" >
@@ -261,12 +320,25 @@ const PROJECT_ROW_OFFSET = 3
261320 ))
262321 }
263322
264- <!-- ── rows 3+: One row per project ── -->
323+ <!-- ── rows 3+: Team headers and project rows ── -->
265324 {
266- projects .map ((proj , pi ) => {
267- const row = pi + PROJECT_ROW_OFFSET
325+ gridItems .map ((item ) => {
326+ if (item .type === ' team-header' ) {
327+ const { team, row, teamColor } = item
328+ return (
329+ <div
330+ class = " team-header"
331+ style = { ` grid-column: 1 / -1; grid-row: ${row }; background: ${teamColor }cc; --team-color: ${teamColor } ` }
332+ >
333+ <span class = " team-color-dot" />
334+ { team .name }
335+ </div >
336+ )
337+ }
338+
339+ const { project : proj, row, teamColor } = item
268340 const projColor = proj .color ?? ' #888'
269- const tintRgb = hexToRgb (proj . color )
341+ const tintRgb = hexToRgb (teamColor )
270342 const iconEmoji = resolveIcon (proj .icon )
271343 const hasDates = !! (proj .startDate && proj .targetDate )
272344 const barLeft = hasDates
@@ -280,7 +352,7 @@ const PROJECT_ROW_OFFSET = 3
280352 : null
281353 const datedMilestones = proj .milestones .filter ((ms ) => ms .targetDate )
282354
283- // Improvement 1: completed project with no targetDate but a completedAt
355+ // Completed project with no targetDate but a completedAt
284356 const hasCompletedBar = !! (
285357 proj .startDate &&
286358 ! proj .targetDate &&
@@ -296,7 +368,7 @@ const PROJECT_ROW_OFFSET = 3
296368 ).toFixed (2 )
297369 : null
298370
299- // Improvement 2: milestones that extend past the project's targetDate
371+ // Milestones that extend past the project's targetDate
300372 const lastMilestoneDate =
301373 datedMilestones .length > 0
302374 ? new Date (
@@ -326,7 +398,7 @@ const PROJECT_ROW_OFFSET = 3
326398 { /* Project label — col 1, explicit row */ }
327399 <div
328400 class = " proj-label"
329- style = { ` grid-column: 1; grid-row: ${row }; --tint-rgb : ${tintRgb } ` }
401+ style = { ` grid-column: 1; grid-row: ${row }; background-color: ${ teamColor }cc; --team-color : ${teamColor } ` }
330402 >
331403 { proj .url ? (
332404 <a
@@ -358,7 +430,7 @@ const PROJECT_ROW_OFFSET = 3
358430 { /* Project timeline track — cols 2 to last, explicit row */ }
359431 <div
360432 class = " proj-track"
361- style = { ` grid-column: 2 / -1; grid-row: ${row }; --tint-rgb : ${tintRgb } ` }
433+ style = { ` grid-column: 2 / -1; grid-row: ${row }; background-color: ${ teamColor }cc; --team-color : ${teamColor } ` }
362434 >
363435 { /* "Today" marker */ }
364436 <div class = " now-line" />
@@ -376,7 +448,7 @@ const PROJECT_ROW_OFFSET = 3
376448 />
377449 )}
378450
379- { /* Improvement 2: milestone overflow extension beyond targetDate */ }
451+ { /* Milestone overflow extension beyond targetDate */ }
380452 { hasExtension && (
381453 <div
382454 class = " proj-bar proj-bar--extension"
@@ -389,7 +461,7 @@ const PROJECT_ROW_OFFSET = 3
389461 />
390462 )}
391463
392- { /* Improvement 1: completed bar (no targetDate, resolved via completedAt) */ }
464+ { /* Completed bar (no targetDate, resolved via completedAt) */ }
393465 { hasCompletedBar && (
394466 <div
395467 class = " proj-bar proj-bar--completed"
@@ -544,6 +616,29 @@ const PROJECT_ROW_OFFSET = 3
544616 letter-spacing: 0.05em;
545617 }
546618
619+ /* ── Team header row ── */
620+ .team-header {
621+ border-top: 2px solid var(--team-color);
622+ border-bottom: 1px solid var(--team-color);
623+ padding: 0.35rem 0.75rem;
624+ display: flex;
625+ align-items: center;
626+ gap: 0.5rem;
627+ font-size: var(--step--2);
628+ font-weight: 700;
629+ text-transform: uppercase;
630+ letter-spacing: 0.06em;
631+ color: #fff;
632+ }
633+
634+ .team-color-dot {
635+ width: 8px;
636+ height: 8px;
637+ border-radius: 50%;
638+ background: rgba(255, 255, 255, 0.6);
639+ flex-shrink: 0;
640+ }
641+
547642 /* ── Project label cell ── */
548643 .proj-label {
549644 position: sticky;
@@ -554,8 +649,7 @@ const PROJECT_ROW_OFFSET = 3
554649 rgb(var(--tint-rgb, 255 255 255)) 5%,
555650 white
556651 );
557- border-right: 1px solid #e5e7eb;
558- border-bottom: 1px solid #f0f0f0;
652+ border-right: 1px solid var(--team-color);
559653 padding: 0 0.75rem;
560654 display: flex;
561655 align-items: center;
@@ -598,20 +692,8 @@ const PROJECT_ROW_OFFSET = 3
598692 rgb(var(--tint-rgb, 255 255 255)) 4%,
599693 white
600694 );
601- border-bottom: 1px solid #f0f0f0;
602695 min-height: 3.25rem;
603696 overflow: visible;
604- /*
605- Repeating vertical lines align with month column boundaries.
606- One segment per month = 100% / n-months wide.
607- */
608- background-image: repeating-linear-gradient(
609- 90deg,
610- transparent 0,
611- transparent calc(100% / var(--n-months) - 1px),
612- #e5e7eb calc(100% / var(--n-months) - 1px),
613- #e5e7eb calc(100% / var(--n-months))
614- );
615697 }
616698
617699 /* "Today" vertical line */
0 commit comments