question-bank-framework 0.8.0 → 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.
Files changed (30) hide show
  1. package/README.md +11 -7
  2. package/framework/components/app-shell.tsx +11 -7
  3. package/framework/components/exam-timer.tsx +36 -27
  4. package/framework/components/filter-builder.tsx +65 -63
  5. package/framework/components/markdown-content.tsx +15 -15
  6. package/framework/components/question-bank.tsx +150 -227
  7. package/framework/components/question-response.tsx +41 -31
  8. package/framework/components/quiz-list.tsx +42 -37
  9. package/framework/components/quiz-page.tsx +63 -62
  10. package/framework/components/quiz-question-editor.tsx +11 -11
  11. package/framework/components/ui/badge.tsx +29 -23
  12. package/framework/components/ui/button.tsx +24 -28
  13. package/framework/components/ui/card.tsx +104 -0
  14. package/framework/components/ui/checkbox.tsx +9 -10
  15. package/framework/components/ui/dropdown-menu.tsx +139 -123
  16. package/framework/components/ui/field.tsx +238 -0
  17. package/framework/components/ui/input.tsx +6 -5
  18. package/framework/components/ui/kbd.tsx +28 -0
  19. package/framework/components/ui/label.tsx +19 -0
  20. package/framework/components/ui/popover.tsx +35 -32
  21. package/framework/components/ui/select.tsx +80 -65
  22. package/framework/components/ui/separator.tsx +24 -0
  23. package/framework/components/ui/sheet.tsx +36 -39
  24. package/framework/components/ui/table.tsx +4 -4
  25. package/framework/components/ui/toast.tsx +2 -2
  26. package/framework/components/ui/tooltip.tsx +65 -0
  27. package/framework/styles/theme.css +66 -28
  28. package/package.json +3 -1
  29. package/template/src/routes/__root.tsx +1 -1
  30. package/template/src/routes/styles.css +0 -4
@@ -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}. Drag to reorder, or remove questions you no longer want.</SheetDescription>
73
- <div className="mt-2 flex items-center justify-between gap-2">
74
- <span className="text-sm text-muted-foreground">
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 py-2 hover:bg-muted',
109
- dragIndex === index && 'border-gray-300 bg-muted opacity-60',
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-sm tabular-nums text-muted-foreground">{index + 1}</span>
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-red-600 hover:bg-red-50 hover:text-red-700"
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. Delete the quiz instead if you no longer need it.
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-red-700">
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
- import * as React from "react"
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 '../../lib/utils.ts'
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-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
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&]:hover:bg-primary/90",
13
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
12
14
  secondary:
13
- "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
15
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
14
16
  destructive:
15
- "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
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&]:hover:bg-accent [a&]:hover:text-accent-foreground",
18
- ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
19
- link: "text-primary underline-offset-4 [a&]:hover:underline",
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
- asChild = false,
34
+ render,
32
35
  ...props
33
- }: React.ComponentProps<"span"> &
34
- VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
- const Comp = asChild ? Slot.Root : "span"
36
-
37
- return (
38
- <Comp
39
- data-slot="badge"
40
- data-variant={variant}
41
- className={cn(badgeVariants({ variant }), className)}
42
- {...props}
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
- import * as React from "react"
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 '../../lib/utils.ts'
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 gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
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/90",
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 shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
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/80",
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-accent hover:text-accent-foreground dark:hover:bg-accent/50",
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: "h-9 px-4 py-2 has-[>svg]:px-3",
24
- xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
25
- sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
26
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
- icon: "size-9",
28
- "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
29
- "icon-sm": "size-8",
30
- "icon-lg": "size-10",
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
- }: React.ComponentProps<"button"> &
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
- <Comp
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
- import * as React from "react"
2
- import { cn } from '../../lib/utils.ts'
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 { Checkbox as CheckboxPrimitive } from "radix-ui"
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 shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary",
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 className="size-3.5" />
22
+ <CheckIcon />
24
23
  </CheckboxPrimitive.Indicator>
25
24
  </CheckboxPrimitive.Root>
26
25
  )