mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(site): add SelectionSummary component and stories for template builder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Jeremy Ruppel
parent
0401ed3af5
commit
09b5e6137f
@@ -0,0 +1,108 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { SelectionSummary } from "./SelectionSummary";
|
||||
|
||||
const meta: Meta<typeof SelectionSummary> = {
|
||||
title: "pages/TemplateBuilder/SelectionSummary",
|
||||
component: SelectionSummary,
|
||||
args: {
|
||||
onDeselectModule: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SelectionSummary>;
|
||||
|
||||
export const NoSelection: Story = {
|
||||
args: {
|
||||
currentStep: 0,
|
||||
selectedTemplate: undefined,
|
||||
selectedModules: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const TemplateSelected: Story = {
|
||||
args: {
|
||||
currentStep: 1,
|
||||
selectedTemplate: {
|
||||
name: "Docker Containers",
|
||||
iconUrl: "/icon/docker.svg",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithModules: Story = {
|
||||
args: {
|
||||
currentStep: 2,
|
||||
selectedTemplate: {
|
||||
name: "Docker Containers",
|
||||
iconUrl: "/icon/docker.svg",
|
||||
},
|
||||
selectedModules: [
|
||||
{
|
||||
id: "jetbrains",
|
||||
name: "Jetbrains",
|
||||
iconUrl: "/icon/jetbrains.svg",
|
||||
},
|
||||
{
|
||||
id: "jetbrains-toolbox",
|
||||
name: "Jetbrains Toolbox",
|
||||
iconUrl: "/icon/jetbrains-toolbox.svg",
|
||||
},
|
||||
{
|
||||
id: "cursor",
|
||||
name: "Cursor IDE",
|
||||
iconUrl: "/icon/cursor.svg",
|
||||
},
|
||||
{
|
||||
id: "claude-code",
|
||||
name: "Claude Code",
|
||||
iconUrl: "/icon/claude.svg",
|
||||
},
|
||||
{
|
||||
id: "filebrowser",
|
||||
name: "File browser",
|
||||
iconUrl: "/icon/filebrowser.svg",
|
||||
},
|
||||
{
|
||||
id: "git-clone",
|
||||
name: "Git clone",
|
||||
iconUrl: "/icon/git.svg",
|
||||
},
|
||||
{
|
||||
id: "devcontainers",
|
||||
name: "Devcontainers",
|
||||
iconUrl: "/icon/devcontainers.svg",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const ManyModules: Story = {
|
||||
args: {
|
||||
currentStep: 2,
|
||||
selectedTemplate: {
|
||||
name: "Docker Containers",
|
||||
iconUrl: "/icon/docker.svg",
|
||||
},
|
||||
selectedModules: Array.from({ length: 12 }, (_, i) => ({
|
||||
id: `module-${i}`,
|
||||
name: `Module ${i + 1}`,
|
||||
iconUrl: "/icon/docker.svg",
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
export const Customizations: Story = {
|
||||
args: {
|
||||
currentStep: 3,
|
||||
selectedTemplate: {
|
||||
name: "Docker Containers",
|
||||
iconUrl: "/icon/docker.svg",
|
||||
},
|
||||
selectedModules: [
|
||||
{ id: "claude-code", name: "Claude Code", iconUrl: "/icon/claude.svg" },
|
||||
{ id: "cursor", name: "Cursor IDE", iconUrl: "/icon/cursor.svg" },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
type SelectedTemplate = {
|
||||
name: string;
|
||||
iconUrl?: string;
|
||||
};
|
||||
|
||||
type SelectedModule = {
|
||||
id: string;
|
||||
name: string;
|
||||
iconUrl: string;
|
||||
};
|
||||
|
||||
type SelectionSummaryProps = {
|
||||
currentStep: number;
|
||||
selectedTemplate?: SelectedTemplate;
|
||||
selectedModules?: SelectedModule[];
|
||||
onDeselectModule: (moduleId: string) => void;
|
||||
};
|
||||
|
||||
export const SelectionSummary: React.FC<SelectionSummaryProps> = ({
|
||||
currentStep,
|
||||
selectedTemplate,
|
||||
selectedModules,
|
||||
onDeselectModule,
|
||||
}) => {
|
||||
const variant = (step: number) => {
|
||||
if (currentStep === step) return "current";
|
||||
if (currentStep > step) return "complete";
|
||||
return "upcoming";
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-semibold">Selection</h2>
|
||||
<div className="flex flex-col">
|
||||
<StepIndicator step={1} variant={variant(1)}>
|
||||
Base Template
|
||||
</StepIndicator>
|
||||
{selectedTemplate ? (
|
||||
<BaseTemplateSelection template={selectedTemplate} />
|
||||
) : (
|
||||
<StepDivider />
|
||||
)}
|
||||
<StepIndicator step={2} variant={variant(2)}>
|
||||
Modules
|
||||
</StepIndicator>
|
||||
{selectedModules ? (
|
||||
<ModuleSelection
|
||||
modules={selectedModules}
|
||||
onDeselectModule={onDeselectModule}
|
||||
/>
|
||||
) : (
|
||||
<StepDivider />
|
||||
)}
|
||||
<StepIndicator step={3} variant={variant(3)}>
|
||||
Customizations
|
||||
</StepIndicator>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StepIndicator: React.FC<{
|
||||
step: number;
|
||||
variant: "complete" | "current" | "upcoming";
|
||||
children: React.ReactNode;
|
||||
}> = ({ step, variant, children }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-full w-8 h-8",
|
||||
"border border-border border-solid",
|
||||
"flex items-center justify-center",
|
||||
variant === "complete" && "border-border-success",
|
||||
variant === "current" && "border-border-primary",
|
||||
variant === "upcoming" &&
|
||||
"border-border border-border-secondary text-content-secondary",
|
||||
)}
|
||||
>
|
||||
{step}
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"font-normal mr-2",
|
||||
variant === "complete" && "text-content-primary",
|
||||
variant === "current" && "text-content-primary",
|
||||
variant === "upcoming" && "text-content-secondary",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StepDivider: React.FC<{
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}> = ({ children, className }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-0 border-l border-border border-solid border-surface-primary mx-4 -translate-x-px",
|
||||
children ? "px-3 py-2" : "h-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BaseTemplateSelection: React.FC<{ template: SelectedTemplate }> = ({
|
||||
template,
|
||||
}) => {
|
||||
return (
|
||||
<StepDivider>
|
||||
<div className="flex items-center p-1">
|
||||
<img
|
||||
src={template.iconUrl}
|
||||
alt={`${template.name} icon`}
|
||||
className="w-6 h-6 p-1 rounded-sm border border-border border-solid bg-surface-secondary"
|
||||
/>
|
||||
<span className="ml-2">{template.name}</span>
|
||||
</div>
|
||||
</StepDivider>
|
||||
);
|
||||
};
|
||||
|
||||
const ModuleSelection: React.FC<{
|
||||
modules: SelectedModule[];
|
||||
onDeselectModule: (moduleId: string) => void;
|
||||
}> = ({ modules, onDeselectModule }) => {
|
||||
return (
|
||||
<StepDivider className="max-h-72 overflow-y-auto">
|
||||
{modules.map((module) => (
|
||||
<div
|
||||
key={module.id}
|
||||
className="group flex items-center justify-between p-1 mb-1 hover:bg-surface-secondary"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={module.iconUrl}
|
||||
alt={`${module.name} icon`}
|
||||
className="w-6 h-6 p-1 rounded-sm border border-border border-solid bg-surface-secondary"
|
||||
/>
|
||||
<span className="ml-2">{module.name}</span>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
|
||||
onClick={() => onDeselectModule(module.id)}
|
||||
>
|
||||
<XIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</StepDivider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user