Skip to content

Commit eaf3c39

Browse files
committed
feat: add wizard components and API for instruction generation
- Introduced WizardAnswerGrid component for displaying answers in a grid format. - Added WizardCompletionSummary component to review selections before generating instructions. - Created WizardConfirmationDialog for user confirmation on actions like reset or file change. - Enhanced icon utilities with support for simple-icons and custom icons. - Implemented instructions API to handle instruction file generation. - Developed wizard response handling functions for managing user selections and responses. - Added summary building functionality to compile user selections for review.
1 parent d1aa817 commit eaf3c39

File tree

8 files changed

+587
-505
lines changed

8 files changed

+587
-505
lines changed

components/instructions-wizard.tsx

Lines changed: 49 additions & 450 deletions
Large diffs are not rendered by default.

components/wizard-answer-grid.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { getAccessibleIconColor, getFallbackInitials, getIconDescriptor, hexToRgba, iconColorOverrides } from "@/lib/icon-utils"
2+
import type { WizardAnswer } from "@/types/wizard"
3+
4+
import { InstructionsAnswerCard } from "./instructions-answer-card"
5+
6+
type WizardAnswerGridProps = {
7+
answers: WizardAnswer[]
8+
onAnswerClick: (answer: WizardAnswer) => void
9+
isSelected: (value: string) => boolean
10+
}
11+
12+
export function WizardAnswerGrid({ answers, onAnswerClick, isSelected }: WizardAnswerGridProps) {
13+
return (
14+
<div className="mt-6 grid gap-3 sm:grid-cols-2">
15+
{answers.map((answer) => {
16+
const iconDescriptor = getIconDescriptor(answer.icon ?? answer.value)
17+
const iconHex = iconDescriptor
18+
? iconColorOverrides[iconDescriptor.slug] ?? iconDescriptor.hex
19+
: undefined
20+
const iconColor = iconHex ? getAccessibleIconColor(iconHex) : undefined
21+
const iconBackgroundColor = iconColor ? hexToRgba(iconColor, 0.18) : null
22+
const iconRingColor = iconColor ? hexToRgba(iconColor, 0.32) : null
23+
const fallbackInitials = getFallbackInitials(answer.label)
24+
const iconElement = iconDescriptor ? (
25+
<span
26+
aria-hidden
27+
className={`flex h-10 w-10 items-center justify-center rounded-xl text-muted-foreground ring-1 ring-border/40${iconColor ? "" : " bg-secondary/40"}`}
28+
style={{
29+
color: iconColor,
30+
backgroundColor: iconBackgroundColor ?? undefined,
31+
boxShadow: iconRingColor ? `inset 0 0 0 1px ${iconRingColor}` : undefined,
32+
}}
33+
>
34+
<span
35+
className="inline-flex h-6 w-6 items-center justify-center text-current [&>svg]:h-full [&>svg]:w-full"
36+
style={{ color: iconColor }}
37+
dangerouslySetInnerHTML={{ __html: iconDescriptor.markup }}
38+
/>
39+
</span>
40+
) : (
41+
<span className="flex h-10 w-10 items-center justify-center rounded-xl bg-secondary/40 text-xs font-semibold uppercase tracking-wide text-muted-foreground ring-1 ring-border/40">
42+
{fallbackInitials}
43+
</span>
44+
)
45+
46+
const hasTooltipContent = Boolean(
47+
(answer.infoLines && answer.infoLines.length > 0) ||
48+
answer.example ||
49+
(answer.tags && answer.tags.length > 0) ||
50+
answer.docs
51+
)
52+
53+
return (
54+
<InstructionsAnswerCard
55+
key={answer.value}
56+
onClick={() => {
57+
void onAnswerClick(answer)
58+
}}
59+
label={answer.label}
60+
iconElement={iconElement}
61+
hasTooltipContent={hasTooltipContent}
62+
infoLines={answer.infoLines}
63+
example={answer.example}
64+
tags={answer.tags}
65+
docs={answer.docs}
66+
selected={isSelected(answer.value)}
67+
disabled={answer.disabled}
68+
disabledLabel={answer.disabledLabel}
69+
/>
70+
)
71+
})}
72+
</div>
73+
)
74+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Button } from "@/components/ui/button"
2+
import type { CompletionSummaryEntry } from "@/lib/wizard-summary"
3+
4+
type WizardCompletionSummaryProps = {
5+
summary: CompletionSummaryEntry[]
6+
onBack: () => void
7+
onGenerate: () => void
8+
isGenerating: boolean
9+
}
10+
11+
export function WizardCompletionSummary({
12+
summary,
13+
onBack,
14+
onGenerate,
15+
isGenerating,
16+
}: WizardCompletionSummaryProps) {
17+
return (
18+
<div className="space-y-6 rounded-3xl border border-border/80 bg-card/95 p-8 shadow-lg">
19+
<div className="space-y-2">
20+
<h2 className="text-2xl font-semibold text-foreground">Review your selections</h2>
21+
<p className="text-sm text-muted-foreground">
22+
Adjust anything before we create your instruction files.
23+
</p>
24+
</div>
25+
26+
<div className="space-y-3">
27+
{summary.map((entry) => (
28+
<div
29+
key={entry.id}
30+
className="rounded-2xl border border-border/70 bg-background/90 p-5"
31+
>
32+
<p className="text-sm font-medium text-muted-foreground">{entry.question}</p>
33+
{entry.hasSelection ? (
34+
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-foreground">
35+
{entry.answers.map((answer) => (
36+
<li key={answer}>{answer}</li>
37+
))}
38+
</ul>
39+
) : (
40+
<p className="mt-2 text-base font-semibold text-foreground">No selection</p>
41+
)}
42+
</div>
43+
))}
44+
</div>
45+
46+
<div className="flex flex-wrap gap-2">
47+
<Button variant="outline" onClick={onBack}>
48+
Back to questions
49+
</Button>
50+
<Button onClick={onGenerate} disabled={isGenerating}>
51+
{isGenerating ? "Generating..." : "Generate My Instructions"}
52+
</Button>
53+
</div>
54+
</div>
55+
)
56+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Button } from "@/components/ui/button"
2+
import type { WizardConfirmationIntent } from "@/types/wizard"
3+
4+
type WizardConfirmationDialogProps = {
5+
intent: WizardConfirmationIntent
6+
onCancel: () => void
7+
onConfirm: () => void
8+
}
9+
10+
const copyByIntent: Record<WizardConfirmationIntent, {
11+
title: string
12+
description: string
13+
confirmLabel: string
14+
}> = {
15+
reset: {
16+
title: "Start over?",
17+
description: "This will clear all of your current selections. Are you sure you want to continue?",
18+
confirmLabel: "Reset Wizard",
19+
},
20+
"change-file": {
21+
title: "Change file?",
22+
description: "Switching files will clear all of your current selections. Are you sure you want to continue?",
23+
confirmLabel: "Change File",
24+
},
25+
}
26+
27+
export function WizardConfirmationDialog({ intent, onCancel, onConfirm }: WizardConfirmationDialogProps) {
28+
const { title, description, confirmLabel } = copyByIntent[intent]
29+
30+
return (
31+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 p-4 backdrop-blur-sm">
32+
<div className="w-full max-w-md space-y-4 rounded-2xl border border-border/70 bg-card/95 p-6 shadow-2xl">
33+
<div className="space-y-2">
34+
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
35+
<p className="text-sm text-muted-foreground">{description}</p>
36+
</div>
37+
<div className="flex flex-wrap justify-end gap-2">
38+
<Button variant="ghost" onClick={onCancel}>
39+
Keep My Answers
40+
</Button>
41+
<Button variant="destructive" onClick={onConfirm}>
42+
{confirmLabel}
43+
</Button>
44+
</div>
45+
</div>
46+
</div>
47+
)
48+
}

0 commit comments

Comments
 (0)