Exercise

Bloc d'exercice interactif pour contenu pedagogique. Installez Exercise depuis le registre VLLNT UI avec la CLI shadcn.

Signaler un bug

Bloc d'exercice interactif pour contenu pedagogique. Fait partie de la famille learning dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Installation

Ajoutez Exercise a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/exercise.json

Source

"use client"; import { useState } from "react"; import { Check, Dumbbell, Eye, EyeOff } from "lucide-react"; import type { ReactNode } from "react"; import type { HeadingTag } from "../../lib/types"; import { cn } from "../../lib/utils"; import { Button } from "../button/button"; const difficultyConfig = { easy: { className: "text-green-600 dark:text-green-400", label: "Easy" }, hard: { className: "text-red-600 dark:text-red-400", label: "Hard" }, medium: { className: "text-amber-600 dark:text-amber-400", label: "Medium" }, }; type HeaderProps = { completed: boolean; config: { className: string; label: string }; Heading: HeadingTag; onToggle: () => void; title: string; }; function ExerciseHeader({ completed, config, Heading, onToggle, title, }: HeaderProps) { return ( <div className="flex items-start justify-between gap-4 mb-4"> <div className="flex items-center gap-3"> <div className="flex size-10 items-center justify-center rounded-full bg-primary/10"> <Dumbbell className="size-5 text-primary" /> </div> <div> <Heading className="font-semibold text-foreground">{title}</Heading> <span className={cn("text-xs font-medium", config.className)}> {config.label} </span> </div> </div> <Button className={cn(completed && "bg-green-500 hover:bg-green-600")} onClick={onToggle} size="sm" variant={completed ? "default" : "outline"} > {completed ? ( <> <Check className="size-4 mr-1" /> Done </> ) : ( "Mark Complete" )} </Button> </div> ); } type HintProps = { hint: string; onShow: () => void; showHint: boolean }; function ExerciseHint({ hint, onShow, showHint }: HintProps) { return ( <div className="mb-4"> {showHint ? ( <div className="p-3 rounded bg-muted/50 text-sm"> <p className="font-medium text-xs uppercase tracking-wider text-muted-foreground mb-1"> Hint </p> <p className="text-muted-foreground italic">{hint}</p> </div> ) : ( <button className="text-sm text-primary hover:underline" onClick={onShow} type="button" > Need a hint? </button> )} </div> ); } type SolutionProps = { onToggle: () => void; showSolution: boolean; solution: ReactNode; }; function ExerciseSolution({ onToggle, showSolution, solution }: SolutionProps) { return ( <div> <Button className="mb-3" onClick={onToggle} size="sm" variant="outline"> {showSolution ? ( <> <EyeOff className="size-4 mr-1" /> Hide Solution </> ) : ( <> <Eye className="size-4 mr-1" /> Show Solution </> )} </Button> {showSolution ? ( <div className="p-4 rounded-lg bg-card border text-sm [&>pre]:my-0"> <p className="font-medium text-xs uppercase tracking-wider text-muted-foreground mb-2"> Solution </p> {solution} </div> ) : null} </div> ); } export type ExerciseProps = { /** Heading tag for the exercise title. Defaults to `h4`. */ as?: HeadingTag; children: ReactNode; difficulty?: "easy" | "hard" | "medium"; hint?: string; solution?: ReactNode; title: string; }; export function Exercise({ as: Heading = "h4", children, difficulty = "medium", hint, solution, title, }: ExerciseProps) { const [showSolution, setShowSolution] = useState(false); const [showHint, setShowHint] = useState(false); const [completed, setCompleted] = useState(false); return ( <div className="my-6 rounded-lg border-2 border-dashed border-primary/30 bg-primary/5 p-6"> <ExerciseHeader completed={completed} config={difficultyConfig[difficulty]} Heading={Heading} onToggle={() => { setCompleted(!completed); }} title={title} /> <div className="text-sm text-muted-foreground mb-4 [&>p]:mb-2 [&>ul]:mb-2"> {children} </div> {hint ? ( <ExerciseHint hint={hint} onShow={() => { setShowHint(true); }} showHint={showHint} /> ) : null} {solution ? ( <ExerciseSolution onToggle={() => { setShowSolution(!showSolution); }} showSolution={showSolution} solution={solution} /> ) : null} </div> ); }

Stories

Explorez chaque variante et etat dans le Storybook interactif :

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Dependances

  • @vllnt/ui@^0.3.0