question-bank-framework 0.7.0 → 0.7.1
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
CHANGED
|
@@ -36,6 +36,10 @@ Your app owns:
|
|
|
36
36
|
@source '../node_modules/question-bank-framework'; /* relative to this file */
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
`theme.css` also pulls in the KaTeX stylesheet from the framework's own `katex`,
|
|
40
|
+
matching the version that renders math. Don't import `katex/dist/katex.min.css`
|
|
41
|
+
yourself — a mismatched KaTeX version breaks math layout (0.19 renamed its CSS classes).
|
|
42
|
+
|
|
39
43
|
Text columns sort number-aware ("Q2" before "Q10"). New quizzes keep the
|
|
40
44
|
table's current sort order, so to get source order add a numeric field (e.g.
|
|
41
45
|
`order`) and sort by it, or set it in `defaultSorting`.
|
|
@@ -1,30 +1,67 @@
|
|
|
1
|
+
import katex from 'katex'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
1
3
|
import ReactMarkdown, { type Components } from 'react-markdown'
|
|
2
|
-
import rehypeKatex from 'rehype-katex'
|
|
3
4
|
import remarkGfm from 'remark-gfm'
|
|
4
5
|
import remarkMath from 'remark-math'
|
|
5
6
|
import { cn } from '../lib/utils.ts'
|
|
6
7
|
|
|
8
|
+
// Math is rendered with the framework's own `katex` dependency rather than
|
|
9
|
+
// rehype-katex, which pins katex@^0.16. KaTeX 0.19 renamed its CSS classes
|
|
10
|
+
// (`.base` → `.katex-base`, …), so 0.16 markup styled by 0.19 CSS loses its
|
|
11
|
+
// layout (subscripts wrap and overlap). theme.css imports the CSS from the
|
|
12
|
+
// same katex package, so markup and stylesheet always agree.
|
|
13
|
+
const textOf = (children: ReactNode): string =>
|
|
14
|
+
Array.isArray(children) ? children.map(textOf).join('') : typeof children === 'string' || typeof children === 'number' ? String(children) : ''
|
|
15
|
+
|
|
16
|
+
const hasClass = (className: unknown, name: string) =>
|
|
17
|
+
(Array.isArray(className) ? className.map(String) : String(className ?? '').split(/\s+/)).includes(name)
|
|
18
|
+
|
|
19
|
+
function MathFormula({ tex, display }: { tex: string; display: boolean }) {
|
|
20
|
+
const html = katex.renderToString(tex, { displayMode: display, throwOnError: false, strict: 'ignore' })
|
|
21
|
+
return display
|
|
22
|
+
? <div dangerouslySetInnerHTML={{ __html: html }} />
|
|
23
|
+
: <span dangerouslySetInnerHTML={{ __html: html }} />
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Every renderer drops react-markdown's `node` prop so it isn't forwarded to the DOM.
|
|
7
27
|
const components: Components = {
|
|
8
|
-
a: ({ ...props }) => <a className="font-medium text-green-800 underline decoration-green-300 underline-offset-2 hover:text-green-950" target="_blank" rel="noreferrer" {...props} />,
|
|
9
|
-
blockquote: ({ ...props }) => <blockquote className="my-4 border-l-2 border-gray-300 pl-4 text-gray-700" {...props} />,
|
|
10
|
-
code: ({ className, children, ...props }) => {
|
|
11
|
-
|
|
12
|
-
|
|
28
|
+
a: ({ node: _node, ...props }) => <a className="font-medium text-green-800 underline decoration-green-300 underline-offset-2 hover:text-green-950" target="_blank" rel="noreferrer" {...props} />,
|
|
29
|
+
blockquote: ({ node: _node, ...props }) => <blockquote className="my-4 border-l-2 border-gray-300 pl-4 text-gray-700" {...props} />,
|
|
30
|
+
code: ({ node: _node, className, children, ...props }) => {
|
|
31
|
+
if (hasClass(className, 'language-math')) {
|
|
32
|
+
return <MathFormula tex={textOf(children)} display={hasClass(className, 'math-display')} />
|
|
33
|
+
}
|
|
34
|
+
// Inline-code styling; fenced blocks reset it via the `pre` renderer below.
|
|
35
|
+
return <code className={cn('rounded bg-gray-100 px-1 py-0.5 font-mono text-[0.9em] text-gray-900 break-words', className)} {...props}>{children}</code>
|
|
13
36
|
},
|
|
14
|
-
h1: ({ ...props }) => <h1 className="mb-3 mt-6 text-2xl font-bold tracking-tight text-gray-950 first:mt-0" {...props} />,
|
|
15
|
-
h2: ({ ...props }) => <h2 className="mb-3 mt-6 text-xl font-bold tracking-tight text-gray-950 first:mt-0" {...props} />,
|
|
16
|
-
h3: ({ ...props }) => <h3 className="mb-2 mt-5 text-lg font-semibold text-gray-950 first:mt-0" {...props} />,
|
|
37
|
+
h1: ({ node: _node, ...props }) => <h1 className="mb-3 mt-6 text-2xl font-bold tracking-tight text-gray-950 first:mt-0" {...props} />,
|
|
38
|
+
h2: ({ node: _node, ...props }) => <h2 className="mb-3 mt-6 text-xl font-bold tracking-tight text-gray-950 first:mt-0" {...props} />,
|
|
39
|
+
h3: ({ node: _node, ...props }) => <h3 className="mb-2 mt-5 text-lg font-semibold text-gray-950 first:mt-0" {...props} />,
|
|
17
40
|
hr: () => <hr className="my-5 border-gray-200" />,
|
|
18
|
-
img: ({ ...props }) => <img className="my-4 h-auto max-w-full rounded-md border border-gray-200" loading="lazy" {...props} />,
|
|
19
|
-
li: ({ ...props }) => <li className="my-1 pl-1" {...props} />,
|
|
20
|
-
ol: ({ ...props }) => <ol className="my-3 list-decimal space-y-1 pl-6" {...props} />,
|
|
21
|
-
p: ({ ...props }) => <p className="my-3 whitespace-pre-wrap leading-7 first:mt-0 last:mb-0" {...props} />,
|
|
22
|
-
pre: ({ children, ...props }) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
img: ({ node: _node, ...props }) => <img className="my-4 h-auto max-w-full rounded-md border border-gray-200" loading="lazy" {...props} />,
|
|
42
|
+
li: ({ node: _node, ...props }) => <li className="my-1 pl-1" {...props} />,
|
|
43
|
+
ol: ({ node: _node, ...props }) => <ol className="my-3 list-decimal space-y-1 pl-6" {...props} />,
|
|
44
|
+
p: ({ node: _node, ...props }) => <p className="my-3 whitespace-pre-wrap leading-7 first:mt-0 last:mb-0" {...props} />,
|
|
45
|
+
pre: ({ node, children, ...props }) => {
|
|
46
|
+
// remark-math emits display math as <pre><code class="language-math math-display">.
|
|
47
|
+
const first = node?.children.find((child) => child.type === 'element')
|
|
48
|
+
if (first?.type === 'element' && hasClass(first.properties.className, 'math-display')) {
|
|
49
|
+
return <div className="my-4 overflow-x-auto overflow-y-hidden py-1">{children}</div>
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
<pre
|
|
53
|
+
className="my-4 overflow-x-auto rounded-lg bg-gray-950 p-4 text-gray-50 [&_code]:rounded-none [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-sm [&_code]:text-inherit [&_code]:break-normal"
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</pre>
|
|
58
|
+
)
|
|
59
|
+
},
|
|
60
|
+
table: ({ node: _node, children, ...props }) => <div className="my-4 overflow-x-auto"><table className="w-full border-collapse text-left text-sm" {...props}>{children}</table></div>,
|
|
61
|
+
tbody: ({ node: _node, ...props }) => <tbody className="divide-y divide-gray-200" {...props} />,
|
|
62
|
+
td: ({ node: _node, ...props }) => <td className="border border-gray-200 px-3 py-2 align-top" {...props} />,
|
|
63
|
+
th: ({ node: _node, ...props }) => <th className="border border-gray-200 bg-gray-50 px-3 py-2 text-left font-semibold" {...props} />,
|
|
64
|
+
ul: ({ node: _node, ...props }) => <ul className="my-3 list-disc space-y-1 pl-6" {...props} />,
|
|
28
65
|
}
|
|
29
66
|
|
|
30
67
|
const markdownCode = /```[\s\S]*?```|~~~[\s\S]*?~~~|(`+)(?!`)[\s\S]*?\1(?!`)/g
|
|
@@ -102,16 +139,16 @@ const normalizeGfmTables = (content: string) => {
|
|
|
102
139
|
// blank labels) can sit inside buttons, select triggers and flex rows.
|
|
103
140
|
const inlineComponents: Components = {
|
|
104
141
|
...components,
|
|
105
|
-
p: ({ ...props }) => <span className="whitespace-pre-wrap" {...props} />,
|
|
142
|
+
p: ({ node: _node, ...props }) => <span className="whitespace-pre-wrap" {...props} />,
|
|
106
143
|
}
|
|
107
144
|
|
|
108
145
|
// min-w-0 + break-words keep long tokens inside the column; display math
|
|
109
|
-
// and wide tables/code scroll horizontally
|
|
110
|
-
const blockClassName = 'min-w-0 max-w-full break-words
|
|
146
|
+
// and wide tables/code scroll horizontally (see the `pre`/`table` renderers).
|
|
147
|
+
const blockClassName = 'min-w-0 max-w-full break-words'
|
|
111
148
|
|
|
112
149
|
export function MarkdownContent({ content, className, inline = false }: { content: string; className?: string; inline?: boolean }) {
|
|
113
150
|
const markdown = (
|
|
114
|
-
<ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]}
|
|
151
|
+
<ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} components={inline ? inlineComponents : components}>
|
|
115
152
|
{normalizeGfmTables(normalizeMathDelimiters(content))}
|
|
116
153
|
</ReactMarkdown>
|
|
117
154
|
)
|
|
@@ -166,19 +166,21 @@ export function PairingResponse({
|
|
|
166
166
|
<li
|
|
167
167
|
key={slot.id}
|
|
168
168
|
className={cn(
|
|
169
|
-
|
|
169
|
+
// Wraps instead of squeezing: short items sit beside their select, while
|
|
170
|
+
// wide items (long formulas) take the full row and the select drops below.
|
|
171
|
+
'flex flex-wrap items-center gap-x-4 gap-y-2 rounded-lg border p-3',
|
|
170
172
|
state === 'idle' && 'border-gray-200 bg-white',
|
|
171
173
|
state === 'correct' && 'border-green-300 bg-green-50/50',
|
|
172
174
|
(state === 'incorrect' || state === 'missing') && 'border-red-300 bg-red-50/50',
|
|
173
175
|
)}
|
|
174
176
|
>
|
|
175
|
-
<div className="flex min-w-0 flex-
|
|
177
|
+
<div className="flex min-w-0 max-w-full flex-[1_1_auto] items-start gap-2">
|
|
176
178
|
<span className="mt-0.5 w-6 shrink-0 text-sm font-semibold tabular-nums text-gray-500">{index + 1}.</span>
|
|
177
|
-
<label htmlFor={selectId} className="min-w-0 flex-1 overflow-x-auto text-base leading-7 text-gray-900">
|
|
179
|
+
<label htmlFor={selectId} className="min-w-0 flex-1 overflow-x-auto overflow-y-hidden text-base leading-7 text-gray-900">
|
|
178
180
|
<MarkdownContent inline content={label} />
|
|
179
181
|
</label>
|
|
180
182
|
</div>
|
|
181
|
-
<div className="flex shrink-0 flex-col gap-1 pl-8 sm:pl-0">
|
|
183
|
+
<div className="ml-auto flex shrink-0 flex-col gap-1 pl-8 sm:pl-0">
|
|
182
184
|
<div className="flex items-center gap-2">
|
|
183
185
|
<OptionSelect
|
|
184
186
|
id={selectId}
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* @import 'question-bank-framework/framework/styles/theme.css';
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/* KaTeX CSS from the framework's own katex, matching the version that renders
|
|
11
|
+
* math in MarkdownContent. Don't import katex CSS separately in your app. */
|
|
12
|
+
@import 'katex/dist/katex.min.css';
|
|
13
|
+
|
|
10
14
|
@theme inline {
|
|
11
15
|
--font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
|
|
12
16
|
--font-display: 'Fraunces', Georgia, serif;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "question-bank-framework",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Framework-style question bank: TanStack Start + TanStack Table grid, quizzes, and filter DSL. Bring your own bank.config.ts + content/. Eject any framework/ file with `qb reveal` (Vike-style).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"lucide-react": "^1.57.0",
|
|
41
41
|
"radix-ui": "^1.8.1",
|
|
42
42
|
"react-markdown": "^10.1.0",
|
|
43
|
-
"rehype-katex": "^7.0.1",
|
|
44
43
|
"remark-gfm": "^4.0.1",
|
|
45
44
|
"remark-math": "^6.0.0",
|
|
46
45
|
"tailwind-merge": "^3.7.0",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,600;9..144,700&display=swap');
|
|
2
|
-
@import 'katex/dist/katex.min.css';
|
|
3
2
|
@import 'tailwindcss';
|
|
4
3
|
@import 'tw-animate-css';
|
|
5
4
|
@import 'question-bank-framework/framework/styles/theme.css';
|