feat(site/src/pages/TemplateBuilder): add scroll overflow and sticky sidebar to wizard steps (#26881)

Add vertical overflow scrolling to the TemplateBuilder wizard step
content areas and make the selection sidebar sticky.

## Changes

- **Scroll on inner content**: Each step's inner content area (below the
heading and any tabs/search) gets `max-h-[calc(100vh-Npx)]
overflow-y-auto`, keeping headings and tab bars visible while the
content scrolls.
- Base template select and module select grids: 420px offset (accounts
for navbar, page header, card padding, tab bar, and nav controls)
  - Base template parameters and module settings: 340px offset
  - Customizations step: no scroll, flows naturally
- **Sticky sidebar**: The selection summary sidebar uses `sticky top-0
self-start` to stay visible while scrolling.
- **Scroll reset on navigation**: `window.scrollTo(0, 0)` in
Back/Continue handlers so users start at the top of each step.

> 🤖 Generated by Coder Agents

<details><summary>Implementation plan</summary>

The approach uses `max-h` with `overflow-y-auto` on the inner content
divs of each step component, placed below the step heading and any
tabs/search controls. This keeps the heading fixed while the content
scrolls. The offset values account for the vertical space consumed by
the navbar, page header, card border/padding, and navigation controls.

The `DashboardFullPage` flex-fill approach was explored but abandoned
because the viewport-constrained card was too short to show content on
smaller screens. The standard `Margins` + `pb-12` layout with per-step
`max-h` provides a better balance.
</details>
This commit is contained in:
Jeremy Ruppel
2026-06-30 20:00:56 -04:00
committed by GitHub
parent 3d966d48b5
commit c56a8f1d99
5 changed files with 38 additions and 27 deletions
@@ -55,7 +55,8 @@ export const BaseInfraSelectStep: FC<BaseInfraSelectStepProps> = ({
Select your infrastructure foundation.
</TemplateBuilderSubtitle>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* 420px accounts for navbar, page header, card padding, tab bar, and nav controls */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[calc(100vh-420px)] overflow-y-auto">
{bases.map((base) => (
<TemplateCard
key={base.id}
@@ -119,29 +119,32 @@ export const BaseTemplateParametersStep: FC<
Your base template requires customizations.
</TemplateBuilderSubtitle>
<TemplateConfiguration
name={base?.name ?? "Base Template"}
description={base?.description ?? ""}
iconUrl={base?.icon}
detailsUrl={detailsUrl(baseId)}
fields={fields}
>
{prerequisites && (
<div className="mt-6">
<MemoizedMarkdown
className={cn(
"text-sm font-normal",
"[&_h2]:mt-6 [&_h2]:text-base [&_h2]:font-semibold",
"[&_h3]:mt-2 [&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-semibold",
"[&_p]:mb-3 [&_p]:text-content-secondary",
"[&_a]:font-normal",
)}
>
{prerequisites}
</MemoizedMarkdown>
</div>
)}
</TemplateConfiguration>
{/* 340px accounts for navbar, page header, card padding, and nav controls */}
<div className="max-h-[calc(100vh-340px)] overflow-y-auto">
<TemplateConfiguration
name={base?.name ?? "Base Template"}
description={base?.description ?? ""}
iconUrl={base?.icon}
detailsUrl={detailsUrl(baseId)}
fields={fields}
>
{prerequisites && (
<div className="mt-6">
<MemoizedMarkdown
className={cn(
"text-sm font-normal",
"[&_h2]:mt-6 [&_h2]:text-base [&_h2]:font-semibold",
"[&_h3]:mt-2 [&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-semibold",
"[&_p]:mb-3 [&_p]:text-content-secondary",
"[&_a]:font-normal",
)}
>
{prerequisites}
</MemoizedMarkdown>
</div>
)}
</TemplateConfiguration>
</div>
</>
);
};
@@ -243,7 +243,8 @@ export const ModuleSelectStep: FC<ModuleSelectStepProps> = ({
</TabsList>
</Tabs>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* 420px accounts for navbar, page header, card padding, search, tabs, and nav controls */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[calc(100vh-420px)] overflow-y-auto">
{visibleModules.length ? (
visibleModules.map((m) => (
<ModuleCard
@@ -127,7 +127,8 @@ export const ModuleSettingsStep: FC<ModuleSettingsStepProps> = ({
Set values for module variables.
</TemplateBuilderSubtitle>
<div className="flex flex-col gap-6">
{/* 340px accounts for navbar, page header, card padding, and nav controls */}
<div className="flex flex-col gap-6 max-h-[calc(100vh-340px)] overflow-y-auto">
{selectedModules.map((mod) => {
const configurableVars = mod.variables.filter((v) => !v.sensitive);
const sensitiveVars = mod.variables.filter((v) => v.sensitive);
@@ -1,4 +1,5 @@
import { type FC, type ReactNode, useReducer, useState } from "react";
import { useQuery } from "react-query";
import { templateBuilderModules } from "#/api/queries/templateBuilder";
import type {
@@ -14,6 +15,7 @@ import {
PageHeaderSubtitle,
PageHeaderTitle,
} from "#/components/PageHeader/PageHeader";
import { docs } from "#/utils/docs";
import { BaseInfraSelectStep } from "./BaseInfraSelectStep";
import {
@@ -68,6 +70,7 @@ export const TemplateBuilderPageView: FC<TemplateBuilderPageViewProps> = ({
const currentIndex = nearestVisible(stepIndex, state);
const currentStep = WIZARD_STEPS[currentIndex];
const nextIndex = findNextVisibleIndex(currentIndex, state);
const prevIndex = findPrevVisibleIndex(currentIndex, state);
const isFirstStep = prevIndex === -1;
@@ -82,6 +85,7 @@ export const TemplateBuilderPageView: FC<TemplateBuilderPageViewProps> = ({
);
const handleBack = () => {
window.scrollTo(0, 0);
setStepIndex(prevIndex);
};
@@ -90,6 +94,7 @@ export const TemplateBuilderPageView: FC<TemplateBuilderPageViewProps> = ({
onCreateTemplate(state);
return;
}
window.scrollTo(0, 0);
setStepIndex(nextIndex);
};
@@ -158,7 +163,7 @@ export const TemplateBuilderPageView: FC<TemplateBuilderPageViewProps> = ({
</div>
{/* Sidebar */}
<div className="w-64 shrink-0 hidden md:block">
<div className="w-64 shrink-0 hidden md:block sticky top-0 self-start">
<SelectionSummary
currentStep={currentStep.group}
selectedTemplate={