@cloudvoyant/helix-svelte 0.16.0 → 0.17.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/dist/Chart.svelte +52 -0
- package/dist/Chart.svelte.d.ts +22 -0
- package/dist/CopyButton.svelte +58 -0
- package/dist/CopyButton.svelte.d.ts +12 -0
- package/dist/Figure.svelte +32 -0
- package/dist/Figure.svelte.d.ts +14 -0
- package/dist/InlineLaTeX.svelte +34 -0
- package/dist/InlineLaTeX.svelte.d.ts +9 -0
- package/dist/LaTeX.svelte +34 -0
- package/dist/LaTeX.svelte.d.ts +9 -0
- package/dist/Manim.svelte +119 -0
- package/dist/Manim.svelte.d.ts +12 -0
- package/dist/Notice.svelte +87 -0
- package/dist/Notice.svelte.d.ts +12 -0
- package/dist/PrevNext.svelte +47 -0
- package/dist/PrevNext.svelte.d.ts +15 -0
- package/dist/Reveal.svelte +40 -0
- package/dist/Reveal.svelte.d.ts +9 -0
- package/dist/YouTube.svelte +85 -0
- package/dist/YouTube.svelte.d.ts +13 -0
- package/dist/YoutubeTimestampAt.svelte +20 -0
- package/dist/YoutubeTimestampAt.svelte.d.ts +10 -0
- package/dist/YoutubeTimestamps.svelte +100 -0
- package/dist/YoutubeTimestamps.svelte.d.ts +11 -0
- package/dist/code-block/CodeBlock.svelte +67 -0
- package/dist/code-block/CodeBlock.svelte.d.ts +17 -0
- package/dist/code-block/CodeBlockContent.svelte +19 -0
- package/dist/code-block/CodeBlockContent.svelte.d.ts +8 -0
- package/dist/code-block/CodeBlockContext.svelte.d.ts +11 -0
- package/dist/code-block/CodeBlockContext.svelte.js +7 -0
- package/dist/code-block/CodeBlockCopyButton.svelte +19 -0
- package/dist/code-block/CodeBlockCopyButton.svelte.d.ts +8 -0
- package/dist/code-block/CodeBlockHeader.svelte +15 -0
- package/dist/code-block/CodeBlockHeader.svelte.d.ts +9 -0
- package/dist/code-block/CodeBlockTitle.svelte +15 -0
- package/dist/code-block/CodeBlockTitle.svelte.d.ts +9 -0
- package/dist/code-block/CodeRenderer.svelte +78 -0
- package/dist/code-block/CodeRenderer.svelte.d.ts +12 -0
- package/dist/code-block/LanguageTabsCodeBlock.svelte +119 -0
- package/dist/code-block/LanguageTabsCodeBlock.svelte.d.ts +12 -0
- package/dist/code-block/MultiFileCodeBlock.svelte +75 -0
- package/dist/code-block/MultiFileCodeBlock.svelte.d.ts +12 -0
- package/dist/code-block/index.d.ts +7 -0
- package/dist/code-block/index.js +8 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/questions/MultipleChoiceQuestion.svelte +167 -0
- package/dist/questions/MultipleChoiceQuestion.svelte.d.ts +19 -0
- package/dist/questions/NumericQuestion.svelte +139 -0
- package/dist/questions/NumericQuestion.svelte.d.ts +20 -0
- package/dist/questions/Quiz.svelte +119 -0
- package/dist/questions/Quiz.svelte.d.ts +11 -0
- package/dist/questions/SingleChoiceQuestion.svelte +141 -0
- package/dist/questions/SingleChoiceQuestion.svelte.d.ts +19 -0
- package/dist/questions/index.d.ts +4 -0
- package/dist/questions/index.js +5 -0
- package/dist/radio-group/RadioGroup.svelte +16 -0
- package/dist/radio-group/RadioGroup.svelte.d.ts +7 -0
- package/dist/radio-group/RadioGroupItem.svelte +26 -0
- package/dist/radio-group/RadioGroupItem.svelte.d.ts +7 -0
- package/dist/radio-group/index.d.ts +2 -0
- package/dist/radio-group/index.js +3 -0
- package/package.json +12 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<!-- libs/helix-svelte/src/code-block/LanguageTabsCodeBlock.svelte -->
|
|
2
|
+
<!-- Closely based on: diffbook code-block family, rebuilt with a helix Select language dropdown
|
|
3
|
+
+ floating copy, mirrored from @cloudvoyant/helix-react -->
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { Ark } from '@ark-ui/svelte/factory';
|
|
6
|
+
import CopyButton from '../CopyButton.svelte';
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
SelectIndicator,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectItemText,
|
|
15
|
+
SelectItemIndicator,
|
|
16
|
+
} from '../select';
|
|
17
|
+
import CodeBlockHeader from './CodeBlockHeader.svelte';
|
|
18
|
+
import CodeBlockTitle from './CodeBlockTitle.svelte';
|
|
19
|
+
import CodeRenderer from './CodeRenderer.svelte';
|
|
20
|
+
import {
|
|
21
|
+
codeBlockRootBase,
|
|
22
|
+
codeBlockCopyFloatBase,
|
|
23
|
+
cn,
|
|
24
|
+
} from '@cloudvoyant/helix';
|
|
25
|
+
import type { LanguageTab } from '@cloudvoyant/helix';
|
|
26
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
27
|
+
|
|
28
|
+
type Props = {
|
|
29
|
+
tabs: LanguageTab[];
|
|
30
|
+
showLineNumbers?: boolean;
|
|
31
|
+
scrollable?: boolean;
|
|
32
|
+
maxHeight?: number;
|
|
33
|
+
class?: string;
|
|
34
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
tabs,
|
|
38
|
+
showLineNumbers = false,
|
|
39
|
+
scrollable = false,
|
|
40
|
+
maxHeight = 400,
|
|
41
|
+
class: className = '',
|
|
42
|
+
...rest
|
|
43
|
+
}: Props = $props();
|
|
44
|
+
|
|
45
|
+
let active = $state(tabs[0]?.label ?? '');
|
|
46
|
+
const tab = $derived(tabs.find((t) => t.label === active) ?? tabs[0]);
|
|
47
|
+
const items = $derived(tabs.map((t) => ({ value: t.label, label: t.label })));
|
|
48
|
+
|
|
49
|
+
$effect(() => {
|
|
50
|
+
if (!tabs.some((t) => t.label === active)) {
|
|
51
|
+
active = tabs[0]?.label ?? '';
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const classes = $derived(cn(codeBlockRootBase, className));
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<Ark as="div" data-code-block class={classes} {...rest}>
|
|
59
|
+
<CodeBlockHeader>
|
|
60
|
+
<CodeBlockTitle>{tab?.filename}</CodeBlockTitle>
|
|
61
|
+
<div class="ml-auto">
|
|
62
|
+
<Select {items} value={[active]} onValueChange={(details) => (active = details.value[0])} aria-label="Language">
|
|
63
|
+
<SelectTrigger size="sm" class="min-w-28 justify-between font-mono text-xs">
|
|
64
|
+
<SelectValue />
|
|
65
|
+
<SelectIndicator>
|
|
66
|
+
<svg
|
|
67
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
68
|
+
viewBox="0 0 24 24"
|
|
69
|
+
fill="none"
|
|
70
|
+
stroke="currentColor"
|
|
71
|
+
stroke-width="2"
|
|
72
|
+
stroke-linecap="round"
|
|
73
|
+
stroke-linejoin="round"
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
>
|
|
76
|
+
<path d="m6 9 6 6 6-6"></path>
|
|
77
|
+
</svg>
|
|
78
|
+
</SelectIndicator>
|
|
79
|
+
</SelectTrigger>
|
|
80
|
+
<SelectContent>
|
|
81
|
+
{#each tabs as t (t.label)}
|
|
82
|
+
<SelectItem item={{ value: t.label, label: t.label }}>
|
|
83
|
+
<SelectItemText>{t.label}</SelectItemText>
|
|
84
|
+
<SelectItemIndicator>
|
|
85
|
+
<svg
|
|
86
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
87
|
+
viewBox="0 0 24 24"
|
|
88
|
+
fill="none"
|
|
89
|
+
stroke="currentColor"
|
|
90
|
+
stroke-width="2"
|
|
91
|
+
stroke-linecap="round"
|
|
92
|
+
stroke-linejoin="round"
|
|
93
|
+
aria-hidden="true"
|
|
94
|
+
>
|
|
95
|
+
<path d="M20 6 9 17l-5-5"></path>
|
|
96
|
+
</svg>
|
|
97
|
+
</SelectItemIndicator>
|
|
98
|
+
</SelectItem>
|
|
99
|
+
{/each}
|
|
100
|
+
</SelectContent>
|
|
101
|
+
</Select>
|
|
102
|
+
</div>
|
|
103
|
+
</CodeBlockHeader>
|
|
104
|
+
{#if tab}
|
|
105
|
+
<div class="relative">
|
|
106
|
+
<CodeRenderer
|
|
107
|
+
code={tab.code}
|
|
108
|
+
language={tab.language ?? 'tsx'}
|
|
109
|
+
html={tab.html}
|
|
110
|
+
{showLineNumbers}
|
|
111
|
+
{scrollable}
|
|
112
|
+
{maxHeight}
|
|
113
|
+
/>
|
|
114
|
+
<div class={codeBlockCopyFloatBase}>
|
|
115
|
+
<CopyButton value={tab.code} label="Copy code" />
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
{/if}
|
|
119
|
+
</Ark>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LanguageTab } from '@cloudvoyant/helix';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Props = {
|
|
4
|
+
tabs: LanguageTab[];
|
|
5
|
+
showLineNumbers?: boolean;
|
|
6
|
+
scrollable?: boolean;
|
|
7
|
+
maxHeight?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
10
|
+
declare const LanguageTabsCodeBlock: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type LanguageTabsCodeBlock = ReturnType<typeof LanguageTabsCodeBlock>;
|
|
12
|
+
export default LanguageTabsCodeBlock;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!-- libs/helix-svelte/src/code-block/MultiFileCodeBlock.svelte -->
|
|
2
|
+
<!-- Closely based on: diffbook code-block family, mirrored from @cloudvoyant/helix-react -->
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { Ark } from '@ark-ui/svelte/factory';
|
|
5
|
+
import CopyButton from '../CopyButton.svelte';
|
|
6
|
+
import CodeRenderer from './CodeRenderer.svelte';
|
|
7
|
+
import { codeBlockRootBase, cn } from '@cloudvoyant/helix';
|
|
8
|
+
import type { FileEntry } from '@cloudvoyant/helix';
|
|
9
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
files: FileEntry[];
|
|
13
|
+
showLineNumbers?: boolean;
|
|
14
|
+
scrollable?: boolean;
|
|
15
|
+
maxHeight?: number;
|
|
16
|
+
class?: string;
|
|
17
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
files,
|
|
21
|
+
showLineNumbers = false,
|
|
22
|
+
scrollable = false,
|
|
23
|
+
maxHeight = 400,
|
|
24
|
+
class: className = '',
|
|
25
|
+
...rest
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let active = $state(files[0]?.filename ?? '');
|
|
29
|
+
const file = $derived(files.find((f) => f.filename === active) ?? files[0]);
|
|
30
|
+
|
|
31
|
+
$effect(() => {
|
|
32
|
+
if (!files.some((f) => f.filename === active)) {
|
|
33
|
+
active = files[0]?.filename ?? '';
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const classes = $derived(cn(codeBlockRootBase, className));
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<Ark as="div" data-code-block class={classes} {...rest}>
|
|
41
|
+
<div class="flex items-center justify-between border-b bg-muted/50">
|
|
42
|
+
<div role="tablist" aria-label="Files" class="flex overflow-x-auto no-scrollbar pl-1">
|
|
43
|
+
{#each files as f (f.filename)}
|
|
44
|
+
<button
|
|
45
|
+
role="tab"
|
|
46
|
+
aria-selected={active === f.filename}
|
|
47
|
+
onclick={() => (active = f.filename)}
|
|
48
|
+
class={cn(
|
|
49
|
+
'h-10 shrink-0 border-b-2 px-3 font-mono text-xs transition-colors',
|
|
50
|
+
active === f.filename
|
|
51
|
+
? 'border-foreground text-foreground'
|
|
52
|
+
: 'border-transparent text-muted-foreground hover:text-foreground',
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{f.filename}
|
|
56
|
+
</button>
|
|
57
|
+
{/each}
|
|
58
|
+
</div>
|
|
59
|
+
{#if file}
|
|
60
|
+
<div class="shrink-0 pr-3">
|
|
61
|
+
<CopyButton value={file.code} label="Copy code" />
|
|
62
|
+
</div>
|
|
63
|
+
{/if}
|
|
64
|
+
</div>
|
|
65
|
+
{#if file}
|
|
66
|
+
<CodeRenderer
|
|
67
|
+
code={file.code}
|
|
68
|
+
language={file.language ?? 'tsx'}
|
|
69
|
+
html={file.html}
|
|
70
|
+
{showLineNumbers}
|
|
71
|
+
{scrollable}
|
|
72
|
+
{maxHeight}
|
|
73
|
+
/>
|
|
74
|
+
{/if}
|
|
75
|
+
</Ark>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FileEntry } from '@cloudvoyant/helix';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Props = {
|
|
4
|
+
files: FileEntry[];
|
|
5
|
+
showLineNumbers?: boolean;
|
|
6
|
+
scrollable?: boolean;
|
|
7
|
+
maxHeight?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
10
|
+
declare const MultiFileCodeBlock: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type MultiFileCodeBlock = ReturnType<typeof MultiFileCodeBlock>;
|
|
12
|
+
export default MultiFileCodeBlock;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as CodeBlock } from './CodeBlock.svelte';
|
|
2
|
+
export { default as CodeBlockHeader } from './CodeBlockHeader.svelte';
|
|
3
|
+
export { default as CodeBlockTitle } from './CodeBlockTitle.svelte';
|
|
4
|
+
export { default as CodeBlockContent } from './CodeBlockContent.svelte';
|
|
5
|
+
export { default as CodeBlockCopyButton } from './CodeBlockCopyButton.svelte';
|
|
6
|
+
export { default as MultiFileCodeBlock } from './MultiFileCodeBlock.svelte';
|
|
7
|
+
export { default as LanguageTabsCodeBlock } from './LanguageTabsCodeBlock.svelte';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// libs/helix-svelte/src/code-block/index.ts
|
|
2
|
+
export { default as CodeBlock } from './CodeBlock.svelte';
|
|
3
|
+
export { default as CodeBlockHeader } from './CodeBlockHeader.svelte';
|
|
4
|
+
export { default as CodeBlockTitle } from './CodeBlockTitle.svelte';
|
|
5
|
+
export { default as CodeBlockContent } from './CodeBlockContent.svelte';
|
|
6
|
+
export { default as CodeBlockCopyButton } from './CodeBlockCopyButton.svelte';
|
|
7
|
+
export { default as MultiFileCodeBlock } from './MultiFileCodeBlock.svelte';
|
|
8
|
+
export { default as LanguageTabsCodeBlock } from './LanguageTabsCodeBlock.svelte';
|
package/dist/index.d.ts
CHANGED
|
@@ -40,3 +40,18 @@ export * from './select';
|
|
|
40
40
|
export * from './combobox';
|
|
41
41
|
export * from './tags-input';
|
|
42
42
|
export { default as Mermaid } from './Mermaid.svelte';
|
|
43
|
+
export { default as CopyButton } from './CopyButton.svelte';
|
|
44
|
+
export * from './radio-group';
|
|
45
|
+
export { default as Notice } from './Notice.svelte';
|
|
46
|
+
export { default as Figure } from './Figure.svelte';
|
|
47
|
+
export { default as Reveal } from './Reveal.svelte';
|
|
48
|
+
export { default as PrevNext } from './PrevNext.svelte';
|
|
49
|
+
export { default as LaTeX } from './LaTeX.svelte';
|
|
50
|
+
export { default as InlineLaTeX } from './InlineLaTeX.svelte';
|
|
51
|
+
export * from './code-block';
|
|
52
|
+
export { default as YouTube } from './YouTube.svelte';
|
|
53
|
+
export { default as YoutubeTimestamps } from './YoutubeTimestamps.svelte';
|
|
54
|
+
export { default as YoutubeTimestampAt } from './YoutubeTimestampAt.svelte';
|
|
55
|
+
export { default as Chart } from './Chart.svelte';
|
|
56
|
+
export { default as Manim } from './Manim.svelte';
|
|
57
|
+
export * from './questions';
|
package/dist/index.js
CHANGED
|
@@ -41,3 +41,18 @@ export * from './select';
|
|
|
41
41
|
export * from './combobox';
|
|
42
42
|
export * from './tags-input';
|
|
43
43
|
export { default as Mermaid } from './Mermaid.svelte';
|
|
44
|
+
export { default as CopyButton } from './CopyButton.svelte';
|
|
45
|
+
export * from './radio-group';
|
|
46
|
+
export { default as Notice } from './Notice.svelte';
|
|
47
|
+
export { default as Figure } from './Figure.svelte';
|
|
48
|
+
export { default as Reveal } from './Reveal.svelte';
|
|
49
|
+
export { default as PrevNext } from './PrevNext.svelte';
|
|
50
|
+
export { default as LaTeX } from './LaTeX.svelte';
|
|
51
|
+
export { default as InlineLaTeX } from './InlineLaTeX.svelte';
|
|
52
|
+
export * from './code-block';
|
|
53
|
+
export { default as YouTube } from './YouTube.svelte';
|
|
54
|
+
export { default as YoutubeTimestamps } from './YoutubeTimestamps.svelte';
|
|
55
|
+
export { default as YoutubeTimestampAt } from './YoutubeTimestampAt.svelte';
|
|
56
|
+
export { default as Chart } from './Chart.svelte';
|
|
57
|
+
export { default as Manim } from './Manim.svelte';
|
|
58
|
+
export * from './questions';
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<!-- libs/helix-svelte/src/questions/MultipleChoiceQuestion.svelte -->
|
|
2
|
+
<!-- Closely based on: diffbook Quiz, mirrored from @cloudvoyant/helix-react -->
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { Ark } from '@ark-ui/svelte/factory';
|
|
5
|
+
import Checkbox from '../checkbox/Checkbox.svelte';
|
|
6
|
+
import CheckboxControl from '../checkbox/CheckboxControl.svelte';
|
|
7
|
+
import CheckboxIndicator from '../checkbox/CheckboxIndicator.svelte';
|
|
8
|
+
import CheckboxLabel from '../checkbox/CheckboxLabel.svelte';
|
|
9
|
+
import {
|
|
10
|
+
questionCardBase,
|
|
11
|
+
questionPromptBase,
|
|
12
|
+
questionGroupBase,
|
|
13
|
+
questionOptionBase,
|
|
14
|
+
questionOptionCorrectBase,
|
|
15
|
+
questionOptionIncorrectBase,
|
|
16
|
+
questionCheckButtonBase,
|
|
17
|
+
feedbackCorrectBase,
|
|
18
|
+
feedbackIncorrectBase,
|
|
19
|
+
feedbackExplanationBase,
|
|
20
|
+
cn,
|
|
21
|
+
} from '@cloudvoyant/helix';
|
|
22
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
id: string;
|
|
26
|
+
prompt: string;
|
|
27
|
+
choices: string[];
|
|
28
|
+
correct: number[];
|
|
29
|
+
explanation?: string;
|
|
30
|
+
quiz?: boolean;
|
|
31
|
+
submitted?: boolean;
|
|
32
|
+
onAnswerChange?: (state: { answered: boolean; correct: boolean }) => void;
|
|
33
|
+
onGraded?: (correct: boolean) => void;
|
|
34
|
+
class?: string;
|
|
35
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
prompt,
|
|
39
|
+
choices,
|
|
40
|
+
correct,
|
|
41
|
+
explanation = undefined,
|
|
42
|
+
quiz = false,
|
|
43
|
+
submitted = false,
|
|
44
|
+
onAnswerChange,
|
|
45
|
+
onGraded,
|
|
46
|
+
class: className = '',
|
|
47
|
+
...rest
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
let selected = $state<Set<number>>(new Set());
|
|
51
|
+
let result = $state<boolean | null>(null);
|
|
52
|
+
|
|
53
|
+
function computeCorrect() {
|
|
54
|
+
const correctSet = new Set(correct);
|
|
55
|
+
return selected.size === correctSet.size && [...selected].every((v) => correctSet.has(v));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const displayResult = $derived(quiz ? (submitted ? computeCorrect() : null) : result);
|
|
59
|
+
|
|
60
|
+
function toggleOption(index: number) {
|
|
61
|
+
if (quiz ? submitted : result !== null) return;
|
|
62
|
+
const next = new Set(selected);
|
|
63
|
+
if (next.has(index)) next.delete(index);
|
|
64
|
+
else next.add(index);
|
|
65
|
+
selected = next;
|
|
66
|
+
if (quiz) {
|
|
67
|
+
const has = selected.size > 0;
|
|
68
|
+
const isCorrect = has && computeCorrect();
|
|
69
|
+
onAnswerChange?.({ answered: has, correct: isCorrect });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function handleCheck() {
|
|
74
|
+
if (selected.size === 0) return;
|
|
75
|
+
const isCorrect = computeCorrect();
|
|
76
|
+
result = isCorrect;
|
|
77
|
+
onGraded?.(isCorrect);
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<div class={cn(questionCardBase, className)} {...rest}>
|
|
82
|
+
<p class={questionPromptBase}>{prompt}</p>
|
|
83
|
+
<div role="group" class={questionGroupBase}>
|
|
84
|
+
{#each choices as choice, i (i)}
|
|
85
|
+
{@const isCorrectOption = correct.includes(i)}
|
|
86
|
+
<Checkbox
|
|
87
|
+
checked={selected.has(i)}
|
|
88
|
+
onCheckedChange={() => toggleOption(i)}
|
|
89
|
+
disabled={quiz ? submitted : result !== null}
|
|
90
|
+
class={cn(
|
|
91
|
+
questionOptionBase,
|
|
92
|
+
displayResult !== null && isCorrectOption && questionOptionCorrectBase,
|
|
93
|
+
displayResult !== null && selected.has(i) && !isCorrectOption && questionOptionIncorrectBase,
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
<CheckboxControl>
|
|
97
|
+
<CheckboxIndicator>
|
|
98
|
+
<svg
|
|
99
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
100
|
+
viewBox="0 0 24 24"
|
|
101
|
+
fill="none"
|
|
102
|
+
stroke="currentColor"
|
|
103
|
+
stroke-width="2"
|
|
104
|
+
stroke-linecap="round"
|
|
105
|
+
stroke-linejoin="round"
|
|
106
|
+
aria-hidden="true"
|
|
107
|
+
>
|
|
108
|
+
<path d="M20 6 9 17l-5-5"></path>
|
|
109
|
+
</svg>
|
|
110
|
+
</CheckboxIndicator>
|
|
111
|
+
</CheckboxControl>
|
|
112
|
+
<CheckboxLabel>{choice}</CheckboxLabel>
|
|
113
|
+
</Checkbox>
|
|
114
|
+
{/each}
|
|
115
|
+
</div>
|
|
116
|
+
{#if !quiz && result === null}
|
|
117
|
+
<Ark as="button" type="button" onclick={handleCheck} disabled={selected.size === 0} class={questionCheckButtonBase}>
|
|
118
|
+
Check
|
|
119
|
+
</Ark>
|
|
120
|
+
{/if}
|
|
121
|
+
{#if displayResult !== null}
|
|
122
|
+
<div aria-live="polite" role="status" class="mt-3">
|
|
123
|
+
<div
|
|
124
|
+
class={cn(
|
|
125
|
+
'flex items-center gap-2 text-sm font-medium',
|
|
126
|
+
displayResult ? feedbackCorrectBase : feedbackIncorrectBase,
|
|
127
|
+
)}
|
|
128
|
+
>
|
|
129
|
+
{#if displayResult}
|
|
130
|
+
<svg
|
|
131
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
132
|
+
viewBox="0 0 24 24"
|
|
133
|
+
fill="none"
|
|
134
|
+
stroke="currentColor"
|
|
135
|
+
stroke-width="2"
|
|
136
|
+
stroke-linecap="round"
|
|
137
|
+
stroke-linejoin="round"
|
|
138
|
+
aria-hidden="true"
|
|
139
|
+
class="size-4 shrink-0"
|
|
140
|
+
>
|
|
141
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
142
|
+
<path d="m9 12 2 2 4-4"></path>
|
|
143
|
+
</svg>
|
|
144
|
+
<span>Correct</span>
|
|
145
|
+
{:else}
|
|
146
|
+
<svg
|
|
147
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
148
|
+
viewBox="0 0 24 24"
|
|
149
|
+
fill="none"
|
|
150
|
+
stroke="currentColor"
|
|
151
|
+
stroke-width="2"
|
|
152
|
+
stroke-linecap="round"
|
|
153
|
+
stroke-linejoin="round"
|
|
154
|
+
aria-hidden="true"
|
|
155
|
+
class="size-4 shrink-0"
|
|
156
|
+
>
|
|
157
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
158
|
+
<path d="m15 9-6 6"></path>
|
|
159
|
+
<path d="m9 9 6 6"></path>
|
|
160
|
+
</svg>
|
|
161
|
+
<span>Incorrect</span>
|
|
162
|
+
{/if}
|
|
163
|
+
</div>
|
|
164
|
+
{#if explanation}<p class={feedbackExplanationBase}>{explanation}</p>{/if}
|
|
165
|
+
</div>
|
|
166
|
+
{/if}
|
|
167
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type Props = {
|
|
3
|
+
id: string;
|
|
4
|
+
prompt: string;
|
|
5
|
+
choices: string[];
|
|
6
|
+
correct: number[];
|
|
7
|
+
explanation?: string;
|
|
8
|
+
quiz?: boolean;
|
|
9
|
+
submitted?: boolean;
|
|
10
|
+
onAnswerChange?: (state: {
|
|
11
|
+
answered: boolean;
|
|
12
|
+
correct: boolean;
|
|
13
|
+
}) => void;
|
|
14
|
+
onGraded?: (correct: boolean) => void;
|
|
15
|
+
class?: string;
|
|
16
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
17
|
+
declare const MultipleChoiceQuestion: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type MultipleChoiceQuestion = ReturnType<typeof MultipleChoiceQuestion>;
|
|
19
|
+
export default MultipleChoiceQuestion;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<!-- libs/helix-svelte/src/questions/NumericQuestion.svelte -->
|
|
2
|
+
<!-- Closely based on: diffbook Quiz, mirrored from @cloudvoyant/helix-react -->
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { Ark } from '@ark-ui/svelte/factory';
|
|
5
|
+
import {
|
|
6
|
+
questionCardBase,
|
|
7
|
+
questionPromptBase,
|
|
8
|
+
questionCheckButtonBase,
|
|
9
|
+
questionInputBase,
|
|
10
|
+
questionUnitBase,
|
|
11
|
+
feedbackCorrectBase,
|
|
12
|
+
feedbackIncorrectBase,
|
|
13
|
+
feedbackExplanationBase,
|
|
14
|
+
cn,
|
|
15
|
+
} from '@cloudvoyant/helix';
|
|
16
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
id: string;
|
|
20
|
+
prompt: string;
|
|
21
|
+
answer: number;
|
|
22
|
+
tolerance?: number;
|
|
23
|
+
unit?: string;
|
|
24
|
+
explanation?: string;
|
|
25
|
+
quiz?: boolean;
|
|
26
|
+
submitted?: boolean;
|
|
27
|
+
onAnswerChange?: (state: { answered: boolean; correct: boolean }) => void;
|
|
28
|
+
onGraded?: (correct: boolean) => void;
|
|
29
|
+
class?: string;
|
|
30
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
id,
|
|
34
|
+
prompt,
|
|
35
|
+
answer,
|
|
36
|
+
tolerance = 0,
|
|
37
|
+
unit,
|
|
38
|
+
explanation = undefined,
|
|
39
|
+
quiz = false,
|
|
40
|
+
submitted = false,
|
|
41
|
+
onAnswerChange,
|
|
42
|
+
onGraded,
|
|
43
|
+
class: className = '',
|
|
44
|
+
...rest
|
|
45
|
+
}: Props = $props();
|
|
46
|
+
|
|
47
|
+
let value = $state('');
|
|
48
|
+
let result = $state<boolean | null>(null);
|
|
49
|
+
|
|
50
|
+
const parsed = $derived(parseFloat(value));
|
|
51
|
+
const computeCorrect = $derived(!Number.isNaN(parsed) && Math.abs(parsed - answer) <= tolerance);
|
|
52
|
+
const displayResult = $derived(quiz ? (submitted ? computeCorrect : null) : result);
|
|
53
|
+
|
|
54
|
+
function handleCheck() {
|
|
55
|
+
if (Number.isNaN(parsed)) return;
|
|
56
|
+
const isCorrect = computeCorrect;
|
|
57
|
+
result = isCorrect;
|
|
58
|
+
onGraded?.(isCorrect);
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<div class={cn(questionCardBase, className)} {...rest}>
|
|
63
|
+
<Ark as="label" for={`q-${id}-input`} class={questionPromptBase}>{prompt}</Ark>
|
|
64
|
+
<div class="flex items-center gap-2">
|
|
65
|
+
<input
|
|
66
|
+
id={`q-${id}-input`}
|
|
67
|
+
type="text"
|
|
68
|
+
inputmode="decimal"
|
|
69
|
+
{value}
|
|
70
|
+
oninput={(e) => {
|
|
71
|
+
if (quiz ? submitted : result !== null) return;
|
|
72
|
+
value = e.currentTarget.value;
|
|
73
|
+
if (quiz) {
|
|
74
|
+
const p = parseFloat(value);
|
|
75
|
+
const has = value.trim() !== '' && !Number.isNaN(p);
|
|
76
|
+
const isCorrect = has && Math.abs(p - answer) <= tolerance;
|
|
77
|
+
onAnswerChange?.({ answered: has, correct: isCorrect });
|
|
78
|
+
}
|
|
79
|
+
}}
|
|
80
|
+
disabled={quiz ? submitted : result !== null}
|
|
81
|
+
class={questionInputBase}
|
|
82
|
+
/>
|
|
83
|
+
{#if unit}<Ark as="span" class={questionUnitBase}>{unit}</Ark>{/if}
|
|
84
|
+
</div>
|
|
85
|
+
{#if !quiz && result === null}
|
|
86
|
+
<Ark
|
|
87
|
+
as="button"
|
|
88
|
+
type="button"
|
|
89
|
+
onclick={handleCheck}
|
|
90
|
+
disabled={value.trim() === '' || Number.isNaN(parsed)}
|
|
91
|
+
class={questionCheckButtonBase}
|
|
92
|
+
>
|
|
93
|
+
Check
|
|
94
|
+
</Ark>
|
|
95
|
+
{/if}
|
|
96
|
+
{#if displayResult !== null}
|
|
97
|
+
<div aria-live="polite" role="status" class="mt-3">
|
|
98
|
+
<div
|
|
99
|
+
class={cn('flex items-center gap-2 text-sm font-medium', displayResult ? feedbackCorrectBase : feedbackIncorrectBase)}
|
|
100
|
+
>
|
|
101
|
+
{#if displayResult}
|
|
102
|
+
<svg
|
|
103
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
104
|
+
viewBox="0 0 24 24"
|
|
105
|
+
fill="none"
|
|
106
|
+
stroke="currentColor"
|
|
107
|
+
stroke-width="2"
|
|
108
|
+
stroke-linecap="round"
|
|
109
|
+
stroke-linejoin="round"
|
|
110
|
+
aria-hidden="true"
|
|
111
|
+
class="size-4 shrink-0"
|
|
112
|
+
>
|
|
113
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
114
|
+
<path d="m9 12 2 2 4-4"></path>
|
|
115
|
+
</svg>
|
|
116
|
+
<span>Correct</span>
|
|
117
|
+
{:else}
|
|
118
|
+
<svg
|
|
119
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
120
|
+
viewBox="0 0 24 24"
|
|
121
|
+
fill="none"
|
|
122
|
+
stroke="currentColor"
|
|
123
|
+
stroke-width="2"
|
|
124
|
+
stroke-linecap="round"
|
|
125
|
+
stroke-linejoin="round"
|
|
126
|
+
aria-hidden="true"
|
|
127
|
+
class="size-4 shrink-0"
|
|
128
|
+
>
|
|
129
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
130
|
+
<path d="m15 9-6 6"></path>
|
|
131
|
+
<path d="m9 9 6 6"></path>
|
|
132
|
+
</svg>
|
|
133
|
+
<span>Incorrect</span>
|
|
134
|
+
{/if}
|
|
135
|
+
</div>
|
|
136
|
+
{#if explanation}<p class={feedbackExplanationBase}>{explanation}</p>{/if}
|
|
137
|
+
</div>
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type Props = {
|
|
3
|
+
id: string;
|
|
4
|
+
prompt: string;
|
|
5
|
+
answer: number;
|
|
6
|
+
tolerance?: number;
|
|
7
|
+
unit?: string;
|
|
8
|
+
explanation?: string;
|
|
9
|
+
quiz?: boolean;
|
|
10
|
+
submitted?: boolean;
|
|
11
|
+
onAnswerChange?: (state: {
|
|
12
|
+
answered: boolean;
|
|
13
|
+
correct: boolean;
|
|
14
|
+
}) => void;
|
|
15
|
+
onGraded?: (correct: boolean) => void;
|
|
16
|
+
class?: string;
|
|
17
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
18
|
+
declare const NumericQuestion: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type NumericQuestion = ReturnType<typeof NumericQuestion>;
|
|
20
|
+
export default NumericQuestion;
|