@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.
Files changed (63) hide show
  1. package/dist/Chart.svelte +52 -0
  2. package/dist/Chart.svelte.d.ts +22 -0
  3. package/dist/CopyButton.svelte +58 -0
  4. package/dist/CopyButton.svelte.d.ts +12 -0
  5. package/dist/Figure.svelte +32 -0
  6. package/dist/Figure.svelte.d.ts +14 -0
  7. package/dist/InlineLaTeX.svelte +34 -0
  8. package/dist/InlineLaTeX.svelte.d.ts +9 -0
  9. package/dist/LaTeX.svelte +34 -0
  10. package/dist/LaTeX.svelte.d.ts +9 -0
  11. package/dist/Manim.svelte +119 -0
  12. package/dist/Manim.svelte.d.ts +12 -0
  13. package/dist/Notice.svelte +87 -0
  14. package/dist/Notice.svelte.d.ts +12 -0
  15. package/dist/PrevNext.svelte +47 -0
  16. package/dist/PrevNext.svelte.d.ts +15 -0
  17. package/dist/Reveal.svelte +40 -0
  18. package/dist/Reveal.svelte.d.ts +9 -0
  19. package/dist/YouTube.svelte +85 -0
  20. package/dist/YouTube.svelte.d.ts +13 -0
  21. package/dist/YoutubeTimestampAt.svelte +20 -0
  22. package/dist/YoutubeTimestampAt.svelte.d.ts +10 -0
  23. package/dist/YoutubeTimestamps.svelte +100 -0
  24. package/dist/YoutubeTimestamps.svelte.d.ts +11 -0
  25. package/dist/code-block/CodeBlock.svelte +67 -0
  26. package/dist/code-block/CodeBlock.svelte.d.ts +17 -0
  27. package/dist/code-block/CodeBlockContent.svelte +19 -0
  28. package/dist/code-block/CodeBlockContent.svelte.d.ts +8 -0
  29. package/dist/code-block/CodeBlockContext.svelte.d.ts +11 -0
  30. package/dist/code-block/CodeBlockContext.svelte.js +7 -0
  31. package/dist/code-block/CodeBlockCopyButton.svelte +19 -0
  32. package/dist/code-block/CodeBlockCopyButton.svelte.d.ts +8 -0
  33. package/dist/code-block/CodeBlockHeader.svelte +15 -0
  34. package/dist/code-block/CodeBlockHeader.svelte.d.ts +9 -0
  35. package/dist/code-block/CodeBlockTitle.svelte +15 -0
  36. package/dist/code-block/CodeBlockTitle.svelte.d.ts +9 -0
  37. package/dist/code-block/CodeRenderer.svelte +78 -0
  38. package/dist/code-block/CodeRenderer.svelte.d.ts +12 -0
  39. package/dist/code-block/LanguageTabsCodeBlock.svelte +119 -0
  40. package/dist/code-block/LanguageTabsCodeBlock.svelte.d.ts +12 -0
  41. package/dist/code-block/MultiFileCodeBlock.svelte +75 -0
  42. package/dist/code-block/MultiFileCodeBlock.svelte.d.ts +12 -0
  43. package/dist/code-block/index.d.ts +7 -0
  44. package/dist/code-block/index.js +8 -0
  45. package/dist/index.d.ts +15 -0
  46. package/dist/index.js +15 -0
  47. package/dist/questions/MultipleChoiceQuestion.svelte +167 -0
  48. package/dist/questions/MultipleChoiceQuestion.svelte.d.ts +19 -0
  49. package/dist/questions/NumericQuestion.svelte +139 -0
  50. package/dist/questions/NumericQuestion.svelte.d.ts +20 -0
  51. package/dist/questions/Quiz.svelte +119 -0
  52. package/dist/questions/Quiz.svelte.d.ts +11 -0
  53. package/dist/questions/SingleChoiceQuestion.svelte +141 -0
  54. package/dist/questions/SingleChoiceQuestion.svelte.d.ts +19 -0
  55. package/dist/questions/index.d.ts +4 -0
  56. package/dist/questions/index.js +5 -0
  57. package/dist/radio-group/RadioGroup.svelte +16 -0
  58. package/dist/radio-group/RadioGroup.svelte.d.ts +7 -0
  59. package/dist/radio-group/RadioGroupItem.svelte +26 -0
  60. package/dist/radio-group/RadioGroupItem.svelte.d.ts +7 -0
  61. package/dist/radio-group/index.d.ts +2 -0
  62. package/dist/radio-group/index.js +3 -0
  63. 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;