mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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={
|
||||
|
||||
Reference in New Issue
Block a user