@moontra/moonui-pro 2.6.0 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +231 -21
- package/dist/index.mjs +14948 -2136
- package/package.json +1 -1
- package/src/components/form-wizard/form-wizard-context.tsx +248 -0
- package/src/components/form-wizard/form-wizard-navigation.tsx +118 -0
- package/src/components/form-wizard/form-wizard-progress.tsx +193 -0
- package/src/components/form-wizard/form-wizard-step.tsx +100 -0
- package/src/components/form-wizard/index.tsx +105 -0
- package/src/components/form-wizard/types.ts +76 -0
- package/src/components/index.ts +10 -1
- package/src/components/multi-step-form/index.tsx +223 -0
- package/src/components/quiz-form/index.tsx +479 -0
- package/src/components/sidebar/index.tsx +19 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback } from "react"
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion"
|
|
5
|
+
import { FormWizardPro, WizardStep } from "../form-wizard"
|
|
6
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
|
7
|
+
import { Button } from "../ui/button"
|
|
8
|
+
import { RadioGroup, RadioGroupItem } from "../ui/radio-group"
|
|
9
|
+
import { Checkbox } from "../ui/checkbox"
|
|
10
|
+
import { Label } from "../ui/label"
|
|
11
|
+
import { Progress } from "../ui/progress"
|
|
12
|
+
import { Slider } from "../ui/slider"
|
|
13
|
+
import { Textarea } from "../ui/textarea"
|
|
14
|
+
import { Badge } from "../ui/badge"
|
|
15
|
+
import { cn } from "../../lib/utils"
|
|
16
|
+
import { Clock, Trophy, Target, ChevronRight, RotateCcw, Download, Share2 } from "lucide-react"
|
|
17
|
+
|
|
18
|
+
export type QuestionType = 'single-choice' | 'multiple-choice' | 'text' | 'rating' | 'true-false' | 'matching'
|
|
19
|
+
|
|
20
|
+
export interface QuizAnswer {
|
|
21
|
+
value: string | string[] | number | boolean
|
|
22
|
+
label?: string
|
|
23
|
+
isCorrect?: boolean
|
|
24
|
+
points?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface QuizQuestion {
|
|
28
|
+
id: string
|
|
29
|
+
type: QuestionType
|
|
30
|
+
question: string
|
|
31
|
+
description?: string
|
|
32
|
+
image?: string
|
|
33
|
+
options?: QuizAnswer[]
|
|
34
|
+
correctAnswer?: string | string[] | number | boolean
|
|
35
|
+
points?: number
|
|
36
|
+
timeLimit?: number // in seconds
|
|
37
|
+
required?: boolean
|
|
38
|
+
explanation?: string
|
|
39
|
+
hint?: string
|
|
40
|
+
tags?: string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface QuizResult {
|
|
44
|
+
questionId: string
|
|
45
|
+
answer: any
|
|
46
|
+
isCorrect?: boolean
|
|
47
|
+
points: number
|
|
48
|
+
timeSpent: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface QuizFormProps {
|
|
52
|
+
title: string
|
|
53
|
+
description?: string
|
|
54
|
+
questions: QuizQuestion[]
|
|
55
|
+
onComplete?: (results: QuizResult[], score: number, totalScore: number) => void | Promise<void>
|
|
56
|
+
showTimer?: boolean
|
|
57
|
+
totalTimeLimit?: number // in seconds
|
|
58
|
+
showProgress?: boolean
|
|
59
|
+
showQuestionNumbers?: boolean
|
|
60
|
+
allowReview?: boolean
|
|
61
|
+
allowSkip?: boolean
|
|
62
|
+
shuffleQuestions?: boolean
|
|
63
|
+
shuffleOptions?: boolean
|
|
64
|
+
passingScore?: number // percentage
|
|
65
|
+
instantFeedback?: boolean
|
|
66
|
+
showHints?: boolean
|
|
67
|
+
className?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function shuffleArray<T>(array: T[]): T[] {
|
|
71
|
+
const shuffled = [...array]
|
|
72
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
73
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
74
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
|
|
75
|
+
}
|
|
76
|
+
return shuffled
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const QuizFormPro: React.FC<QuizFormProps> = ({
|
|
80
|
+
title,
|
|
81
|
+
description,
|
|
82
|
+
questions: initialQuestions,
|
|
83
|
+
onComplete,
|
|
84
|
+
showTimer = true,
|
|
85
|
+
totalTimeLimit,
|
|
86
|
+
showProgress = true,
|
|
87
|
+
showQuestionNumbers = true,
|
|
88
|
+
allowReview = true,
|
|
89
|
+
allowSkip = true,
|
|
90
|
+
shuffleQuestions = false,
|
|
91
|
+
shuffleOptions = false,
|
|
92
|
+
passingScore = 70,
|
|
93
|
+
instantFeedback = false,
|
|
94
|
+
showHints = false,
|
|
95
|
+
className
|
|
96
|
+
}) => {
|
|
97
|
+
const [questions] = useState(() =>
|
|
98
|
+
shuffleQuestions ? shuffleArray(initialQuestions) : initialQuestions
|
|
99
|
+
)
|
|
100
|
+
const [answers, setAnswers] = useState<Record<string, any>>({})
|
|
101
|
+
const [results, setResults] = useState<QuizResult[]>([])
|
|
102
|
+
const [timeRemaining, setTimeRemaining] = useState(totalTimeLimit || 0)
|
|
103
|
+
const [questionStartTime, setQuestionStartTime] = useState<Record<string, number>>({})
|
|
104
|
+
const [showResults, setShowResults] = useState(false)
|
|
105
|
+
const [currentQuestionTime, setCurrentQuestionTime] = useState(0)
|
|
106
|
+
|
|
107
|
+
// Timer effect
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!showTimer || !totalTimeLimit || showResults) return
|
|
110
|
+
|
|
111
|
+
const interval = setInterval(() => {
|
|
112
|
+
setTimeRemaining(prev => {
|
|
113
|
+
if (prev <= 1) {
|
|
114
|
+
handleQuizComplete()
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
return prev - 1
|
|
118
|
+
})
|
|
119
|
+
}, 1000)
|
|
120
|
+
|
|
121
|
+
return () => clearInterval(interval)
|
|
122
|
+
}, [showTimer, totalTimeLimit, showResults])
|
|
123
|
+
|
|
124
|
+
// Current question timer
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!showTimer || showResults) return
|
|
127
|
+
|
|
128
|
+
const interval = setInterval(() => {
|
|
129
|
+
setCurrentQuestionTime(prev => prev + 1)
|
|
130
|
+
}, 1000)
|
|
131
|
+
|
|
132
|
+
return () => clearInterval(interval)
|
|
133
|
+
}, [showTimer, showResults])
|
|
134
|
+
|
|
135
|
+
const calculateScore = useCallback(() => {
|
|
136
|
+
let score = 0
|
|
137
|
+
let totalScore = 0
|
|
138
|
+
|
|
139
|
+
const quizResults: QuizResult[] = questions.map(question => {
|
|
140
|
+
const answer = answers[question.id]
|
|
141
|
+
const points = question.points || 1
|
|
142
|
+
totalScore += points
|
|
143
|
+
|
|
144
|
+
let isCorrect = false
|
|
145
|
+
if (answer !== undefined && answer !== null && answer !== '') {
|
|
146
|
+
if (question.type === 'single-choice' || question.type === 'true-false') {
|
|
147
|
+
isCorrect = answer === question.correctAnswer
|
|
148
|
+
} else if (question.type === 'multiple-choice') {
|
|
149
|
+
const userAnswers = Array.isArray(answer) ? answer : []
|
|
150
|
+
const correctAnswers = Array.isArray(question.correctAnswer) ? question.correctAnswer : []
|
|
151
|
+
isCorrect = userAnswers.length === correctAnswers.length &&
|
|
152
|
+
userAnswers.every(a => correctAnswers.includes(a))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const earnedPoints = isCorrect ? points : 0
|
|
157
|
+
score += earnedPoints
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
questionId: question.id,
|
|
161
|
+
answer,
|
|
162
|
+
isCorrect,
|
|
163
|
+
points: earnedPoints,
|
|
164
|
+
timeSpent: questionStartTime[question.id] || 0
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return { results: quizResults, score, totalScore }
|
|
169
|
+
}, [questions, answers, questionStartTime])
|
|
170
|
+
|
|
171
|
+
const handleQuizComplete = async () => {
|
|
172
|
+
const { results, score, totalScore } = calculateScore()
|
|
173
|
+
setResults(results)
|
|
174
|
+
setShowResults(true)
|
|
175
|
+
|
|
176
|
+
if (onComplete) {
|
|
177
|
+
await onComplete(results, score, totalScore)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const formatTime = (seconds: number) => {
|
|
182
|
+
const mins = Math.floor(seconds / 60)
|
|
183
|
+
const secs = seconds % 60
|
|
184
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const wizardSteps: WizardStep[] = questions.map((question, index) => ({
|
|
188
|
+
id: question.id,
|
|
189
|
+
title: `Question ${index + 1}`,
|
|
190
|
+
description: question.tags?.join(', '),
|
|
191
|
+
content: ({ updateStepData, goToNext }) => {
|
|
192
|
+
const currentAnswer = answers[question.id]
|
|
193
|
+
const questionOptions = shuffleOptions && question.options
|
|
194
|
+
? shuffleArray(question.options)
|
|
195
|
+
: question.options
|
|
196
|
+
|
|
197
|
+
// Track time for this question
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!questionStartTime[question.id]) {
|
|
200
|
+
setQuestionStartTime(prev => ({
|
|
201
|
+
...prev,
|
|
202
|
+
[question.id]: currentQuestionTime
|
|
203
|
+
}))
|
|
204
|
+
}
|
|
205
|
+
}, [])
|
|
206
|
+
|
|
207
|
+
const handleAnswerChange = (value: any) => {
|
|
208
|
+
setAnswers(prev => ({ ...prev, [question.id]: value }))
|
|
209
|
+
if (instantFeedback && question.type !== 'text') {
|
|
210
|
+
// Show feedback immediately
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<Card className="border-0 shadow-none">
|
|
216
|
+
<CardHeader>
|
|
217
|
+
<div className="flex items-center justify-between mb-2">
|
|
218
|
+
{showQuestionNumbers && (
|
|
219
|
+
<Badge variant="secondary">
|
|
220
|
+
Question {index + 1} of {questions.length}
|
|
221
|
+
</Badge>
|
|
222
|
+
)}
|
|
223
|
+
{question.points && question.points > 1 && (
|
|
224
|
+
<Badge variant="outline">
|
|
225
|
+
{question.points} points
|
|
226
|
+
</Badge>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
<CardTitle className="text-xl">{question.question}</CardTitle>
|
|
230
|
+
{question.description && (
|
|
231
|
+
<CardDescription>{question.description}</CardDescription>
|
|
232
|
+
)}
|
|
233
|
+
</CardHeader>
|
|
234
|
+
<CardContent className="space-y-4">
|
|
235
|
+
{question.image && (
|
|
236
|
+
<img
|
|
237
|
+
src={question.image}
|
|
238
|
+
alt="Question illustration"
|
|
239
|
+
className="w-full rounded-lg"
|
|
240
|
+
/>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{question.type === 'single-choice' && questionOptions && (
|
|
244
|
+
<RadioGroup
|
|
245
|
+
value={currentAnswer || ''}
|
|
246
|
+
onValueChange={handleAnswerChange}
|
|
247
|
+
>
|
|
248
|
+
{questionOptions.map((option, optionIndex) => (
|
|
249
|
+
<motion.div
|
|
250
|
+
key={String(option.value)}
|
|
251
|
+
initial={{ opacity: 0, x: -20 }}
|
|
252
|
+
animate={{ opacity: 1, x: 0 }}
|
|
253
|
+
transition={{ delay: optionIndex * 0.1 }}
|
|
254
|
+
className="flex items-center space-x-2 p-3 rounded-lg hover:bg-muted/50 transition-colors"
|
|
255
|
+
>
|
|
256
|
+
<RadioGroupItem value={String(option.value)} id={`${question.id}-${option.value}`} />
|
|
257
|
+
<Label
|
|
258
|
+
htmlFor={`${question.id}-${option.value}`}
|
|
259
|
+
className="flex-1 cursor-pointer"
|
|
260
|
+
>
|
|
261
|
+
{option.label || option.value}
|
|
262
|
+
</Label>
|
|
263
|
+
</motion.div>
|
|
264
|
+
))}
|
|
265
|
+
</RadioGroup>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{question.type === 'multiple-choice' && questionOptions && (
|
|
269
|
+
<div className="space-y-2">
|
|
270
|
+
{questionOptions.map((option, optionIndex) => (
|
|
271
|
+
<motion.div
|
|
272
|
+
key={String(option.value)}
|
|
273
|
+
initial={{ opacity: 0, x: -20 }}
|
|
274
|
+
animate={{ opacity: 1, x: 0 }}
|
|
275
|
+
transition={{ delay: optionIndex * 0.1 }}
|
|
276
|
+
className="flex items-center space-x-2 p-3 rounded-lg hover:bg-muted/50 transition-colors"
|
|
277
|
+
>
|
|
278
|
+
<Checkbox
|
|
279
|
+
id={`${question.id}-${option.value}`}
|
|
280
|
+
checked={(currentAnswer || []).includes(option.value)}
|
|
281
|
+
onCheckedChange={(checked) => {
|
|
282
|
+
const current = currentAnswer || []
|
|
283
|
+
if (checked) {
|
|
284
|
+
handleAnswerChange([...current, option.value])
|
|
285
|
+
} else {
|
|
286
|
+
handleAnswerChange(current.filter((v: string) => v !== option.value))
|
|
287
|
+
}
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
<Label
|
|
291
|
+
htmlFor={`${question.id}-${option.value}`}
|
|
292
|
+
className="flex-1 cursor-pointer"
|
|
293
|
+
>
|
|
294
|
+
{option.label || option.value}
|
|
295
|
+
</Label>
|
|
296
|
+
</motion.div>
|
|
297
|
+
))}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
{question.type === 'true-false' && (
|
|
302
|
+
<RadioGroup
|
|
303
|
+
value={currentAnswer?.toString() || ''}
|
|
304
|
+
onValueChange={(value) => handleAnswerChange(value === 'true')}
|
|
305
|
+
>
|
|
306
|
+
<div className="flex items-center space-x-2 p-3 rounded-lg hover:bg-muted/50">
|
|
307
|
+
<RadioGroupItem value="true" id={`${question.id}-true`} />
|
|
308
|
+
<Label htmlFor={`${question.id}-true`} className="flex-1 cursor-pointer">
|
|
309
|
+
True
|
|
310
|
+
</Label>
|
|
311
|
+
</div>
|
|
312
|
+
<div className="flex items-center space-x-2 p-3 rounded-lg hover:bg-muted/50">
|
|
313
|
+
<RadioGroupItem value="false" id={`${question.id}-false`} />
|
|
314
|
+
<Label htmlFor={`${question.id}-false`} className="flex-1 cursor-pointer">
|
|
315
|
+
False
|
|
316
|
+
</Label>
|
|
317
|
+
</div>
|
|
318
|
+
</RadioGroup>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
{question.type === 'text' && (
|
|
322
|
+
<Textarea
|
|
323
|
+
value={currentAnswer || ''}
|
|
324
|
+
onChange={(e) => handleAnswerChange(e.target.value)}
|
|
325
|
+
placeholder="Type your answer here..."
|
|
326
|
+
className="min-h-[120px]"
|
|
327
|
+
/>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{question.type === 'rating' && (
|
|
331
|
+
<div className="space-y-4">
|
|
332
|
+
<Slider
|
|
333
|
+
value={[currentAnswer || 5]}
|
|
334
|
+
onValueChange={([value]) => handleAnswerChange(value)}
|
|
335
|
+
max={10}
|
|
336
|
+
min={1}
|
|
337
|
+
step={1}
|
|
338
|
+
className="w-full"
|
|
339
|
+
/>
|
|
340
|
+
<div className="text-center text-2xl font-bold">
|
|
341
|
+
{currentAnswer || 5}/10
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
|
|
346
|
+
{showHints && question.hint && (
|
|
347
|
+
<Alert>
|
|
348
|
+
<AlertDescription>
|
|
349
|
+
<strong>Hint:</strong> {question.hint}
|
|
350
|
+
</AlertDescription>
|
|
351
|
+
</Alert>
|
|
352
|
+
)}
|
|
353
|
+
</CardContent>
|
|
354
|
+
</Card>
|
|
355
|
+
)
|
|
356
|
+
},
|
|
357
|
+
validation: () => {
|
|
358
|
+
if (question.required && !answers[question.id]) {
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
return true
|
|
362
|
+
},
|
|
363
|
+
isOptional: !question.required
|
|
364
|
+
}))
|
|
365
|
+
|
|
366
|
+
if (showResults) {
|
|
367
|
+
const { score, totalScore } = calculateScore()
|
|
368
|
+
const percentage = Math.round((score / totalScore) * 100)
|
|
369
|
+
const passed = percentage >= passingScore
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<motion.div
|
|
373
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
374
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
375
|
+
className={cn("w-full max-w-2xl mx-auto", className)}
|
|
376
|
+
>
|
|
377
|
+
<Card>
|
|
378
|
+
<CardHeader className="text-center">
|
|
379
|
+
<div className="mx-auto mb-4">
|
|
380
|
+
<Trophy className={cn(
|
|
381
|
+
"w-16 h-16",
|
|
382
|
+
passed ? "text-yellow-500" : "text-muted-foreground"
|
|
383
|
+
)} />
|
|
384
|
+
</div>
|
|
385
|
+
<CardTitle className="text-3xl">Quiz Complete!</CardTitle>
|
|
386
|
+
<CardDescription>
|
|
387
|
+
Here's how you performed
|
|
388
|
+
</CardDescription>
|
|
389
|
+
</CardHeader>
|
|
390
|
+
<CardContent className="space-y-6">
|
|
391
|
+
<div className="text-center">
|
|
392
|
+
<div className="text-5xl font-bold mb-2">
|
|
393
|
+
{percentage}%
|
|
394
|
+
</div>
|
|
395
|
+
<Progress value={percentage} className="h-3 mb-4" />
|
|
396
|
+
<div className="flex justify-between text-sm text-muted-foreground">
|
|
397
|
+
<span>Score: {score}/{totalScore}</span>
|
|
398
|
+
<span>
|
|
399
|
+
{passed ? (
|
|
400
|
+
<Badge variant="success">Passed</Badge>
|
|
401
|
+
) : (
|
|
402
|
+
<Badge variant="destructive">Failed</Badge>
|
|
403
|
+
)}
|
|
404
|
+
</span>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
{allowReview && (
|
|
409
|
+
<div className="space-y-2">
|
|
410
|
+
<h3 className="font-semibold">Review Answers</h3>
|
|
411
|
+
{results.map((result, index) => {
|
|
412
|
+
const question = questions.find(q => q.id === result.questionId)
|
|
413
|
+
return (
|
|
414
|
+
<div key={result.questionId} className="flex items-center justify-between p-2 rounded-lg bg-muted/50">
|
|
415
|
+
<span className="text-sm">
|
|
416
|
+
{showQuestionNumbers && `Q${index + 1}: `}
|
|
417
|
+
{question?.question.substring(0, 50)}...
|
|
418
|
+
</span>
|
|
419
|
+
<Badge variant={result.isCorrect ? "success" : "destructive"}>
|
|
420
|
+
{result.points}/{question?.points || 1}
|
|
421
|
+
</Badge>
|
|
422
|
+
</div>
|
|
423
|
+
)
|
|
424
|
+
})}
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
|
|
428
|
+
<div className="flex gap-2">
|
|
429
|
+
<Button className="flex-1" onClick={() => window.location.reload()}>
|
|
430
|
+
<RotateCcw className="w-4 h-4 mr-2" />
|
|
431
|
+
Retake Quiz
|
|
432
|
+
</Button>
|
|
433
|
+
<Button variant="outline">
|
|
434
|
+
<Download className="w-4 h-4 mr-2" />
|
|
435
|
+
Download Results
|
|
436
|
+
</Button>
|
|
437
|
+
<Button variant="outline">
|
|
438
|
+
<Share2 className="w-4 h-4 mr-2" />
|
|
439
|
+
Share
|
|
440
|
+
</Button>
|
|
441
|
+
</div>
|
|
442
|
+
</CardContent>
|
|
443
|
+
</Card>
|
|
444
|
+
</motion.div>
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<div className={cn("w-full", className)}>
|
|
450
|
+
{showTimer && totalTimeLimit && (
|
|
451
|
+
<div className="mb-6 flex items-center justify-between">
|
|
452
|
+
<div className="flex items-center gap-2">
|
|
453
|
+
<Clock className="w-4 h-4" />
|
|
454
|
+
<span className="text-sm font-medium">
|
|
455
|
+
Time Remaining: {formatTime(timeRemaining)}
|
|
456
|
+
</span>
|
|
457
|
+
</div>
|
|
458
|
+
<Progress
|
|
459
|
+
value={(timeRemaining / totalTimeLimit) * 100}
|
|
460
|
+
className="w-32 h-2"
|
|
461
|
+
/>
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
|
|
465
|
+
<FormWizardPro
|
|
466
|
+
steps={wizardSteps}
|
|
467
|
+
onComplete={handleQuizComplete}
|
|
468
|
+
allowStepSkip={allowSkip}
|
|
469
|
+
validateOnStepChange={true}
|
|
470
|
+
showProgressBar={showProgress}
|
|
471
|
+
progressType="dots"
|
|
472
|
+
animationType="slide"
|
|
473
|
+
/>
|
|
474
|
+
</div>
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Additional Alert import since it was missing
|
|
479
|
+
import { Alert, AlertDescription } from "../ui/alert"
|
|
@@ -153,6 +153,13 @@ export interface SidebarConfig {
|
|
|
153
153
|
collapsible?: boolean
|
|
154
154
|
defaultCollapsed?: boolean
|
|
155
155
|
floatingActionButton?: boolean
|
|
156
|
+
floatingActionButtonPosition?: {
|
|
157
|
+
bottom?: string
|
|
158
|
+
left?: string
|
|
159
|
+
right?: string
|
|
160
|
+
top?: string
|
|
161
|
+
}
|
|
162
|
+
floatingActionButtonClassName?: string
|
|
156
163
|
glassmorphism?: boolean
|
|
157
164
|
animatedBackground?: boolean
|
|
158
165
|
keyboardShortcuts?: boolean
|
|
@@ -196,6 +203,8 @@ export function Sidebar({
|
|
|
196
203
|
collapsible = true,
|
|
197
204
|
defaultCollapsed = false,
|
|
198
205
|
floatingActionButton = true,
|
|
206
|
+
floatingActionButtonPosition = { bottom: '1rem', left: '1rem' },
|
|
207
|
+
floatingActionButtonClassName,
|
|
199
208
|
glassmorphism = false,
|
|
200
209
|
animatedBackground = false,
|
|
201
210
|
keyboardShortcuts = true,
|
|
@@ -803,8 +812,17 @@ export function Sidebar({
|
|
|
803
812
|
{floatingActionButton && (
|
|
804
813
|
<Button
|
|
805
814
|
onClick={() => setIsOpen(true)}
|
|
806
|
-
className=
|
|
815
|
+
className={cn(
|
|
816
|
+
"fixed z-40 h-12 w-12 rounded-full shadow-lg md:hidden",
|
|
817
|
+
floatingActionButtonClassName
|
|
818
|
+
)}
|
|
807
819
|
size="icon"
|
|
820
|
+
style={{
|
|
821
|
+
bottom: floatingActionButtonPosition?.bottom,
|
|
822
|
+
left: floatingActionButtonPosition?.left,
|
|
823
|
+
right: floatingActionButtonPosition?.right,
|
|
824
|
+
top: floatingActionButtonPosition?.top
|
|
825
|
+
}}
|
|
808
826
|
>
|
|
809
827
|
<Menu className="h-5 w-5" />
|
|
810
828
|
</Button>
|