question-bank-framework 0.7.1 → 0.9.0
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/README.md +22 -7
- package/framework/components/app-shell.tsx +40 -26
- package/framework/components/exam-timer.tsx +134 -0
- package/framework/components/filter-builder.tsx +65 -63
- package/framework/components/markdown-content.tsx +15 -15
- package/framework/components/question-bank.tsx +150 -227
- package/framework/components/question-response.tsx +41 -31
- package/framework/components/quiz-list.tsx +42 -37
- package/framework/components/quiz-page.tsx +63 -62
- package/framework/components/quiz-question-editor.tsx +11 -11
- package/framework/components/ui/badge.tsx +29 -23
- package/framework/components/ui/button.tsx +24 -28
- package/framework/components/ui/card.tsx +104 -0
- package/framework/components/ui/checkbox.tsx +9 -10
- package/framework/components/ui/dropdown-menu.tsx +139 -123
- package/framework/components/ui/field.tsx +238 -0
- package/framework/components/ui/input.tsx +6 -5
- package/framework/components/ui/kbd.tsx +28 -0
- package/framework/components/ui/label.tsx +19 -0
- package/framework/components/ui/popover.tsx +35 -32
- package/framework/components/ui/select.tsx +80 -65
- package/framework/components/ui/separator.tsx +24 -0
- package/framework/components/ui/sheet.tsx +36 -39
- package/framework/components/ui/table.tsx +4 -4
- package/framework/components/ui/toast.tsx +234 -0
- package/framework/components/ui/tooltip.tsx +65 -0
- package/framework/index.ts +4 -0
- package/framework/lib/exam-timer.ts +115 -0
- package/framework/styles/theme.css +66 -28
- package/package.json +4 -1
- package/template/src/routes/__root.tsx +1 -1
- package/template/src/routes/styles.css +0 -4
- package/template/src/routes/timer.tsx +4 -0
|
@@ -68,10 +68,10 @@ export function QuizQuestionEditor({
|
|
|
68
68
|
<Sheet open={quiz !== null} onOpenChange={onOpenChange}>
|
|
69
69
|
<SheetContent className="w-full gap-0 sm:max-w-md">
|
|
70
70
|
<SheetHeader className="border-b">
|
|
71
|
-
<SheetTitle>Edit questions</SheetTitle>
|
|
72
|
-
<SheetDescription>{quiz?.title}
|
|
73
|
-
<div className="mt-
|
|
74
|
-
<span className="text-
|
|
71
|
+
<SheetTitle className="font-semibold">Edit questions</SheetTitle>
|
|
72
|
+
<SheetDescription className="truncate">{quiz?.title}</SheetDescription>
|
|
73
|
+
<div className="mt-3 flex items-center justify-between gap-2">
|
|
74
|
+
<span className="text-xs tabular-nums text-muted-foreground">
|
|
75
75
|
{ids.length} {ids.length === 1 ? 'question' : 'questions'}
|
|
76
76
|
</span>
|
|
77
77
|
<Button
|
|
@@ -83,7 +83,7 @@ export function QuizQuestionEditor({
|
|
|
83
83
|
aria-label="Shuffle question order"
|
|
84
84
|
title="Shuffle question order"
|
|
85
85
|
>
|
|
86
|
-
<Shuffle aria-hidden="true" />
|
|
86
|
+
<Shuffle aria-hidden="true" data-icon="inline-start" />
|
|
87
87
|
Shuffle
|
|
88
88
|
</Button>
|
|
89
89
|
</div>
|
|
@@ -105,12 +105,12 @@ export function QuizQuestionEditor({
|
|
|
105
105
|
}}
|
|
106
106
|
onDragEnd={() => setDragIndex(null)}
|
|
107
107
|
className={cn(
|
|
108
|
-
'flex items-center gap-2 rounded-md border border-transparent px-2
|
|
109
|
-
dragIndex === index && 'border-
|
|
108
|
+
'flex h-10 items-center gap-2 rounded-md border border-transparent px-2 hover:bg-muted/50',
|
|
109
|
+
dragIndex === index && 'border-border bg-muted opacity-60',
|
|
110
110
|
)}
|
|
111
111
|
>
|
|
112
112
|
<GripVertical aria-hidden="true" className="size-4 shrink-0 cursor-grab text-muted-foreground" />
|
|
113
|
-
<span className="w-6 shrink-0 text-right text-
|
|
113
|
+
<span className="w-6 shrink-0 text-right text-xs tabular-nums text-muted-foreground">{index + 1}</span>
|
|
114
114
|
<span className="min-w-0 flex-1 truncate text-sm">{bank.questionById.get(id)?.title ?? id}</span>
|
|
115
115
|
<Button
|
|
116
116
|
type="button"
|
|
@@ -137,7 +137,7 @@ export function QuizQuestionEditor({
|
|
|
137
137
|
variant="ghost"
|
|
138
138
|
size="icon-xs"
|
|
139
139
|
aria-label="Remove question"
|
|
140
|
-
className="text-
|
|
140
|
+
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
|
141
141
|
onClick={() => setIds((current) => current.filter((item) => item !== id))}
|
|
142
142
|
>
|
|
143
143
|
<Trash2 aria-hidden="true" />
|
|
@@ -146,13 +146,13 @@ export function QuizQuestionEditor({
|
|
|
146
146
|
))}
|
|
147
147
|
{ids.length === 0 && (
|
|
148
148
|
<li className="px-2 py-8 text-center text-sm text-muted-foreground">
|
|
149
|
-
No questions left
|
|
149
|
+
No questions left
|
|
150
150
|
</li>
|
|
151
151
|
)}
|
|
152
152
|
</ol>
|
|
153
153
|
<SheetFooter className="border-t">
|
|
154
154
|
{saveError && (
|
|
155
|
-
<p role="alert" className="m-0 text-sm text-
|
|
155
|
+
<p role="alert" className="m-0 text-sm text-destructive">
|
|
156
156
|
Could not save changes. Please try again.
|
|
157
157
|
</p>
|
|
158
158
|
)}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
// shadcn/ui badge (Base UI, base-nova). Framework-owned: eject with `qb reveal framework/components/ui/badge.tsx`.
|
|
2
|
+
|
|
3
|
+
import { mergeProps } from "@base-ui/react/merge-props"
|
|
4
|
+
import { useRender } from "@base-ui/react/use-render"
|
|
2
5
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
-
import { cn } from
|
|
4
|
-
import { Slot } from "radix-ui"
|
|
6
|
+
import { cn } from "../../lib/utils.ts"
|
|
5
7
|
|
|
6
8
|
const badgeVariants = cva(
|
|
7
|
-
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-
|
|
9
|
+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
|
8
10
|
{
|
|
9
11
|
variants: {
|
|
10
12
|
variant: {
|
|
11
|
-
default: "bg-primary text-primary-foreground [a
|
|
13
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
12
14
|
secondary:
|
|
13
|
-
"bg-secondary text-secondary-foreground [a
|
|
15
|
+
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
14
16
|
destructive:
|
|
15
|
-
"bg-destructive text-
|
|
17
|
+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
|
16
18
|
outline:
|
|
17
|
-
"border-border text-foreground [a
|
|
18
|
-
ghost:
|
|
19
|
-
|
|
19
|
+
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
20
|
+
ghost:
|
|
21
|
+
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
22
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
20
23
|
},
|
|
21
24
|
},
|
|
22
25
|
defaultVariants: {
|
|
@@ -28,20 +31,23 @@ const badgeVariants = cva(
|
|
|
28
31
|
function Badge({
|
|
29
32
|
className,
|
|
30
33
|
variant = "default",
|
|
31
|
-
|
|
34
|
+
render,
|
|
32
35
|
...props
|
|
33
|
-
}:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
37
|
+
return useRender({
|
|
38
|
+
defaultTagName: "span",
|
|
39
|
+
props: mergeProps<"span">(
|
|
40
|
+
{
|
|
41
|
+
className: cn(badgeVariants({ variant }), className),
|
|
42
|
+
},
|
|
43
|
+
props
|
|
44
|
+
),
|
|
45
|
+
render,
|
|
46
|
+
state: {
|
|
47
|
+
slot: "badge",
|
|
48
|
+
variant,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
export { Badge, badgeVariants }
|
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
// shadcn/ui button (Base UI, base-nova). Framework-owned: eject with `qb reveal framework/components/ui/button.tsx`.
|
|
2
|
+
|
|
3
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
|
2
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
-
import { cn } from
|
|
4
|
-
import { Slot } from "radix-ui"
|
|
5
|
+
import { cn } from "../../lib/utils.ts"
|
|
5
6
|
|
|
6
7
|
const buttonVariants = cva(
|
|
7
|
-
"inline-flex shrink-0 items-center justify-center
|
|
8
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
8
9
|
{
|
|
9
10
|
variants: {
|
|
10
11
|
variant: {
|
|
11
|
-
default: "bg-primary text-primary-foreground hover:bg-primary/
|
|
12
|
-
destructive:
|
|
13
|
-
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
14
13
|
outline:
|
|
15
|
-
"border bg-background
|
|
14
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
16
15
|
secondary:
|
|
17
|
-
"bg-secondary text-secondary-foreground hover:bg-secondary
|
|
16
|
+
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
18
17
|
ghost:
|
|
19
|
-
"hover:bg-
|
|
18
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
19
|
+
destructive:
|
|
20
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
20
21
|
link: "text-primary underline-offset-4 hover:underline",
|
|
21
22
|
},
|
|
22
23
|
size: {
|
|
23
|
-
default:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"icon-
|
|
30
|
-
|
|
24
|
+
default:
|
|
25
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
26
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
27
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
28
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
29
|
+
icon: "size-8",
|
|
30
|
+
"icon-xs":
|
|
31
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
32
|
+
"icon-sm":
|
|
33
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
34
|
+
"icon-lg": "size-9",
|
|
31
35
|
},
|
|
32
36
|
},
|
|
33
37
|
defaultVariants: {
|
|
@@ -41,19 +45,11 @@ function Button({
|
|
|
41
45
|
className,
|
|
42
46
|
variant = "default",
|
|
43
47
|
size = "default",
|
|
44
|
-
asChild = false,
|
|
45
48
|
...props
|
|
46
|
-
}:
|
|
47
|
-
VariantProps<typeof buttonVariants> & {
|
|
48
|
-
asChild?: boolean
|
|
49
|
-
}) {
|
|
50
|
-
const Comp = asChild ? Slot.Root : "button"
|
|
51
|
-
|
|
49
|
+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
52
50
|
return (
|
|
53
|
-
<
|
|
51
|
+
<ButtonPrimitive
|
|
54
52
|
data-slot="button"
|
|
55
|
-
data-variant={variant}
|
|
56
|
-
data-size={size}
|
|
57
53
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
58
54
|
{...props}
|
|
59
55
|
/>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// shadcn/ui card (Base UI, base-nova). Framework-owned: eject with `qb reveal framework/components/ui/card.tsx`.
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../lib/utils.ts"
|
|
5
|
+
|
|
6
|
+
function Card({
|
|
7
|
+
className,
|
|
8
|
+
size = "default",
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
data-slot="card"
|
|
14
|
+
data-size={size}
|
|
15
|
+
className={cn(
|
|
16
|
+
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
data-slot="card-header"
|
|
28
|
+
className={cn(
|
|
29
|
+
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
data-slot="card-title"
|
|
41
|
+
className={cn(
|
|
42
|
+
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
data-slot="card-description"
|
|
54
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
data-slot="card-action"
|
|
64
|
+
className={cn(
|
|
65
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
66
|
+
className
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
data-slot="card-content"
|
|
77
|
+
className={cn("px-(--card-spacing)", className)}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
data-slot="card-footer"
|
|
87
|
+
className={cn(
|
|
88
|
+
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
Card,
|
|
98
|
+
CardHeader,
|
|
99
|
+
CardFooter,
|
|
100
|
+
CardTitle,
|
|
101
|
+
CardAction,
|
|
102
|
+
CardDescription,
|
|
103
|
+
CardContent,
|
|
104
|
+
}
|
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// shadcn/ui checkbox (Base UI, base-nova). Framework-owned: eject with `qb reveal framework/components/ui/checkbox.tsx`.
|
|
2
|
+
|
|
3
|
+
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
|
|
3
4
|
import { CheckIcon } from "lucide-react"
|
|
4
|
-
import {
|
|
5
|
+
import { cn } from "../../lib/utils.ts"
|
|
6
|
+
|
|
5
7
|
|
|
6
|
-
function Checkbox({
|
|
7
|
-
className,
|
|
8
|
-
...props
|
|
9
|
-
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
8
|
+
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|
10
9
|
return (
|
|
11
10
|
<CheckboxPrimitive.Root
|
|
12
11
|
data-slot="checkbox"
|
|
13
12
|
className={cn(
|
|
14
|
-
"peer size-4 shrink-0 rounded-[4px] border border-input
|
|
13
|
+
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary",
|
|
15
14
|
className
|
|
16
15
|
)}
|
|
17
16
|
{...props}
|
|
18
17
|
>
|
|
19
18
|
<CheckboxPrimitive.Indicator
|
|
20
19
|
data-slot="checkbox-indicator"
|
|
21
|
-
className="grid place-content-center text-current transition-none"
|
|
20
|
+
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
|
22
21
|
>
|
|
23
|
-
<CheckIcon
|
|
22
|
+
<CheckIcon />
|
|
24
23
|
</CheckboxPrimitive.Indicator>
|
|
25
24
|
</CheckboxPrimitive.Root>
|
|
26
25
|
)
|