@examplary/cli 1.7.0 → 1.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 (45) hide show
  1. package/dist/commands/preview.d.ts +5 -0
  2. package/dist/commands/preview.js +174 -0
  3. package/dist/commands/upload.d.ts +5 -0
  4. package/dist/commands/upload.js +138 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +37 -0
  7. package/dist/lib/api.d.ts +5 -0
  8. package/dist/lib/api.js +47 -0
  9. package/dist/lib/bundle.d.ts +4 -0
  10. package/dist/lib/bundle.js +27 -0
  11. package/dist/lib/jsonata.d.ts +1 -0
  12. package/dist/lib/jsonata.js +26 -0
  13. package/dist/lib/read-meta.d.ts +16 -0
  14. package/dist/lib/read-meta.js +36 -0
  15. package/dist/lib/styles.d.ts +1 -0
  16. package/dist/lib/styles.js +24 -0
  17. package/dist/preview-ui/assets/icon-nodes-Ds7bJID4.js +2 -0
  18. package/dist/preview-ui/assets/icon-nodes-Ds7bJID4.js.map +1 -0
  19. package/dist/preview-ui/assets/index-DcEcr1Zm.css +1 -0
  20. package/dist/preview-ui/assets/index-NlxMWEJQ.js +3629 -0
  21. package/dist/preview-ui/assets/index-NlxMWEJQ.js.map +1 -0
  22. package/dist/preview-ui/assets/tags-DZBm0mOh.js +2 -0
  23. package/dist/preview-ui/assets/tags-DZBm0mOh.js.map +1 -0
  24. package/{src → dist}/preview-ui/index.html +2 -1
  25. package/package.json +23 -22
  26. package/bin/index.js +0 -37
  27. package/src/commands/preview.js +0 -220
  28. package/src/commands/upload.js +0 -179
  29. package/src/lib/api.js +0 -53
  30. package/src/lib/bundle.js +0 -35
  31. package/src/lib/jsonata.js +0 -31
  32. package/src/lib/middleware.js +0 -16
  33. package/src/lib/read-meta.js +0 -40
  34. package/src/lib/styles.js +0 -32
  35. package/src/preview-ui/App.tsx +0 -148
  36. package/src/preview-ui/components/json-editor.tsx +0 -27
  37. package/src/preview-ui/components/preview-assessment.tsx +0 -43
  38. package/src/preview-ui/components/preview-print.tsx +0 -38
  39. package/src/preview-ui/components/preview-results.tsx +0 -38
  40. package/src/preview-ui/components/preview-settings-area.tsx +0 -97
  41. package/src/preview-ui/components/question-component.tsx +0 -75
  42. package/src/preview-ui/index.css +0 -11
  43. package/src/preview-ui/lib/demo-api.ts +0 -29
  44. package/src/preview-ui/lib/i18n.ts +0 -15
  45. package/src/preview-ui/main.tsx +0 -8
@@ -1,40 +0,0 @@
1
- import { readFile } from "fs/promises";
2
- import { join } from "path";
3
- import { cwd } from "process";
4
-
5
- import yaml from "yaml";
6
-
7
- /**
8
- * Reads and parses the question-type definition file.
9
- * Prefers question-type.json over question-type.yml.
10
- */
11
- export const readMetaFile = async () => {
12
- const dir = cwd();
13
- const jsonPath = join(dir, "question-type.json");
14
- const ymlPath = join(dir, "question-type.yml");
15
-
16
- // Try JSON first, then YAML
17
- for (const { path, parse, format } of [
18
- { path: jsonPath, parse: JSON.parse, format: "JSON" },
19
- { path: ymlPath, parse: yaml.parse, format: "YAML" },
20
- ]) {
21
- let contents;
22
- try {
23
- contents = await readFile(path, "utf-8");
24
- } catch {
25
- continue;
26
- }
27
-
28
- try {
29
- const definition = parse(contents);
30
- if (definition.$schema) delete definition.$schema;
31
- return { definition, path };
32
- } catch (error) {
33
- throw new Error(`Invalid ${format} in ${path}: ${error.message}`);
34
- }
35
- }
36
-
37
- throw new Error(
38
- "No question-type.json or question-type.yml found in the current directory",
39
- );
40
- };
package/src/lib/styles.js DELETED
@@ -1,32 +0,0 @@
1
- import { createRequire } from "module";
2
- import path from "path";
3
-
4
- import tailwindcss from "@tailwindcss/postcss";
5
- import postcss from "postcss";
6
-
7
- const require = createRequire(import.meta.url);
8
- const tailwindDir = path.dirname(require.resolve("tailwindcss/package.json"));
9
-
10
- export const buildStyles = async () => {
11
- const absolutePath = path.resolve(tailwindDir);
12
-
13
- const inputCSS = `
14
- @import "${absolutePath}/theme.css" layer(theme);
15
- @import "${absolutePath}/utilities.css" layer(utilities);
16
- `;
17
-
18
- const result = await postcss([
19
- tailwindcss({
20
- optimize: { minify: true },
21
- }),
22
- ]).process(inputCSS, { from: "module-css.css" });
23
-
24
- const utilitiesStyles = result.css.split("@layer utilities")[1].trim();
25
- if (utilitiesStyles.length < 5) {
26
- return null;
27
- }
28
-
29
- const css = result.css.replace(/@layer /g, "@layer modules_");
30
-
31
- return css;
32
- };
@@ -1,148 +0,0 @@
1
- import { useState } from "react";
2
- import { useTranslation } from "react-i18next";
3
-
4
- import type { QuestionType } from "@examplary/schemas";
5
- import {
6
- formatQuestionType,
7
- Select,
8
- SelectContent,
9
- SelectItem,
10
- SelectTrigger,
11
- SelectValue,
12
- } from "@examplary/ui";
13
- import useLocalStorage from "use-local-storage";
14
-
15
- import { JsonEditor } from "./components/json-editor";
16
- import { PreviewAssessment } from "./components/preview-assessment";
17
- import { PreviewPrint } from "./components/preview-print";
18
- import { PreviewResults } from "./components/preview-results";
19
- import { PreviewSettingsArea } from "./components/preview-settings-area";
20
-
21
- function App() {
22
- const { i18n } = useTranslation();
23
-
24
- const rawQuestionType = globalThis._questionType as unknown as QuestionType;
25
- const questionType = formatQuestionType(rawQuestionType, i18n);
26
-
27
- const [componentName, setComponentName] = useLocalStorage(
28
- `component-name-${questionType.id}`,
29
- Object.keys(questionType.components || {})[0] || "assessment",
30
- );
31
-
32
- const availableComponents = Object.keys(questionType.components || {});
33
-
34
- const [answer, setAnswer] = useState(undefined);
35
-
36
- const defaultSettings = (questionType.settings || []).reduce(
37
- (acc, setting) => {
38
- if ("default" in setting) {
39
- acc[setting.id] = setting.default;
40
- }
41
- return acc;
42
- },
43
- {},
44
- );
45
-
46
- const [question, setQuestion] = useLocalStorage(
47
- `payload-question-${questionType.id}`,
48
- {
49
- id: "q_qQjXQasmUfpWRV5KO5mzkxoy",
50
- title: "Napoleon's role in the French Revolution",
51
- type: questionType.id,
52
- description:
53
- "When Napoleon Bonaparte rose to power, he was seen as a hero of the French Revolution. However, his actions and policies during his rule have been debated by historians. What was Napoleon's role in the French Revolution?",
54
- settings: {
55
- ...defaultSettings,
56
- options: [
57
- "To establish a centralized legal system and promote the principles of the Revolution.",
58
- "To overthrow the revolutionary government and establish a dictatorship.",
59
- "To maintain the status quo and prevent further revolutionary changes.",
60
- "To support the monarchy and restore the old regime.",
61
- ],
62
- correctAnswer:
63
- "To establish a centralized legal system and promote the principles of the Revolution.",
64
- },
65
- },
66
- );
67
-
68
- return (
69
- <div className="min-h-screen flex">
70
- <aside className="bg-nav w-[360px] text-black border-r-2 border-border p-6 overflow-auto">
71
- <h1 className="text-xl font-semibold">{questionType.name}</h1>
72
- <h2 className="text-sm opacity-50">Question type preview</h2>
73
-
74
- <h3 className="text-sm font-medium mb-1.5 mt-8">Preview options</h3>
75
- <div className="grid grid-cols-3 items-center gap-3 border border-border-accent rounded-md bg-white p-4">
76
- <p className="text-gray-800 text-sm">Component</p>
77
- <Select value={componentName} onValueChange={setComponentName}>
78
- <SelectTrigger className="text-sm col-span-2">
79
- <SelectValue />
80
- </SelectTrigger>
81
- <SelectContent>
82
- {availableComponents.map((component) => (
83
- <SelectItem key={component} value={component}>
84
- {component}
85
- </SelectItem>
86
- ))}
87
- </SelectContent>
88
- </Select>
89
- <p className="text-gray-800 text-sm">Locale</p>
90
- <Select
91
- value={i18n.language}
92
- onValueChange={(value) => i18n.changeLanguage(value)}
93
- >
94
- <SelectTrigger className="text-sm col-span-2">
95
- <SelectValue />
96
- </SelectTrigger>
97
- <SelectContent>
98
- <SelectItem value="en">en</SelectItem>
99
- <SelectItem value="nl">nl</SelectItem>
100
- </SelectContent>
101
- </Select>
102
- </div>
103
-
104
- <h3 className="text-sm font-medium mb-1.5 mt-8">Question</h3>
105
- <div className="border border-border-accent rounded-md bg-white overflow-hidden">
106
- <JsonEditor value={question} setValue={setQuestion} />
107
- </div>
108
-
109
- <h3 className="text-sm font-medium mb-1.5 mt-8">Answer</h3>
110
- <div className="border border-border-accent rounded-md bg-white overflow-hidden">
111
- <JsonEditor value={answer} setValue={setAnswer} />
112
- </div>
113
- </aside>
114
-
115
- <div className="flex-1 flex items-center justify-center">
116
- <div className="max-w-3xl mx-auto py-10" key={i18n.language}>
117
- {componentName === "assessment" && (
118
- <PreviewAssessment
119
- questionType={questionType}
120
- question={question}
121
- answer={answer}
122
- saveAnswer={setAnswer}
123
- />
124
- )}
125
- {componentName === "results" && (
126
- <PreviewResults
127
- questionType={questionType}
128
- question={question}
129
- answer={answer}
130
- />
131
- )}
132
- {componentName === "settings-area" && (
133
- <PreviewSettingsArea
134
- questionType={questionType}
135
- question={question}
136
- setQuestion={setQuestion}
137
- />
138
- )}
139
- {componentName === "print" && (
140
- <PreviewPrint questionType={questionType} question={question} />
141
- )}
142
- </div>
143
- </div>
144
- </div>
145
- );
146
- }
147
-
148
- export default App;
@@ -1,27 +0,0 @@
1
- import { useEffect, useState } from "react";
2
-
3
- export const JsonEditor = ({ value, setValue }) => {
4
- const [stringValue, setStringValue] = useState(() =>
5
- JSON.stringify(value, null, 2),
6
- );
7
- useEffect(() => {
8
- setStringValue(JSON.stringify(value, null, 2));
9
- }, [value]);
10
-
11
- return (
12
- <textarea
13
- className="h-[160px] font-mono text-xs p-3 w-full"
14
- value={stringValue}
15
- onChange={(e) => {
16
- const newStringValue = e.target.value;
17
- setStringValue(newStringValue || "");
18
- try {
19
- const parsedValue = JSON.parse(newStringValue || "");
20
- setValue(parsedValue);
21
- } catch (error) {
22
- console.error("Invalid JSON:", error);
23
- }
24
- }}
25
- />
26
- );
27
- };
@@ -1,43 +0,0 @@
1
- import { RichTextDisplay } from "@examplary/ui";
2
-
3
- import { QuestionComponent } from "./question-component";
4
- import { api } from "../lib/demo-api";
5
-
6
- export const PreviewAssessment = ({
7
- questionType,
8
- question,
9
- answer,
10
- saveAnswer,
11
- }) => {
12
- return (
13
- <div className="relative flex flex-col min-h-[calc(100vh-17.5rem)]">
14
- <div className="rounded-xl border border-border bg-main text-black flex-1 bg-white py-7 px-10 relative z-10">
15
- <div className="flex justify-between gap-2 mb-3">
16
- <div className="size-6 bg-bright border text-sm font-bold rounded-full flex items-center justify-center shrink-0">
17
- 1
18
- </div>
19
- <div className="flex-1">
20
- <h3 className="font-semibold text-lg mb-1">
21
- <RichTextDisplay as="span">{question.title}</RichTextDisplay>
22
- </h3>
23
- <RichTextDisplay as="p" className="text-zinc-900 mb-6">
24
- {question.description}
25
- </RichTextDisplay>
26
-
27
- <QuestionComponent
28
- type={questionType}
29
- componentName="assessment"
30
- props={{
31
- environment: "exam",
32
- api,
33
- question,
34
- answer,
35
- saveAnswer,
36
- }}
37
- />
38
- </div>
39
- </div>
40
- </div>
41
- </div>
42
- );
43
- };
@@ -1,38 +0,0 @@
1
- import { RichTextDisplay } from "@examplary/ui";
2
-
3
- import { QuestionComponent } from "./question-component";
4
-
5
- export const PreviewPrint = ({ questionType, question }) => {
6
- return (
7
- <div className="relative">
8
- <div className="border-2 border-border border-t-0 border-b-0 bg-main text-black flex-1 bg-white">
9
- <div className="flex justify-between gap-2 mb-3 py-16 px-12">
10
- <div className="size-6 border-2 text-sm font-bold rounded-full flex items-center justify-center shrink-0">
11
- 1
12
- </div>
13
- <div className="flex-1">
14
- <h3 className="font-semibold text-lg mb-1">
15
- <RichTextDisplay as="span">{question.title}</RichTextDisplay>
16
- </h3>
17
- <RichTextDisplay as="p" className="text-zinc-900 mb-6">
18
- {question.description}
19
- </RichTextDisplay>
20
-
21
- <QuestionComponent
22
- type={questionType}
23
- componentName="print"
24
- props={{
25
- environment: "exam",
26
- question,
27
- answerBoxes: true,
28
- }}
29
- />
30
- </div>
31
- </div>
32
- </div>
33
-
34
- <div className="absolute inset-0 bottom-auto h-10 bg-gradient-to-b from-bg to-transparent z-10" />
35
- <div className="absolute inset-0 top-auto h-10 bg-gradient-to-b to-bg from-transparent z-10" />
36
- </div>
37
- );
38
- };
@@ -1,38 +0,0 @@
1
- import { RichTextDisplay } from "@examplary/ui";
2
-
3
- import { QuestionComponent } from "./question-component";
4
- import { api } from "../lib/demo-api";
5
-
6
- export const PreviewResults = ({ questionType, question, answer }) => {
7
- return (
8
- <div className="rounded-base border-2 border-border bg-main text-black flex-1 bg-white py-7 px-10 md:mr-5 relative z-10">
9
- <div className="flex justify-between gap-2 mb-3">
10
- <div className="size-6 bg-bright border-2 text-sm font-bold rounded-full flex items-center justify-center shrink-0">
11
- 1
12
- </div>
13
- <div className="flex-1">
14
- <h3 className="font-semibold text-lg mb-2.5">
15
- <RichTextDisplay as="span">{question.title}</RichTextDisplay>
16
- </h3>
17
-
18
- {answer ? (
19
- <QuestionComponent
20
- type={questionType}
21
- componentName="results"
22
- props={{
23
- environment: "exam",
24
- api,
25
- question,
26
- answer,
27
- }}
28
- />
29
- ) : (
30
- <span className="text-zinc-500 bg-zinc-100 rounded p-1 px-2 font-medium text-sm">
31
- Empty answer
32
- </span>
33
- )}
34
- </div>
35
- </div>
36
- </div>
37
- );
38
- };
@@ -1,97 +0,0 @@
1
- import { cn, MinimalRichTextField } from "@examplary/ui";
2
- import { FileQuestionIcon } from "lucide-react";
3
-
4
- import { QuestionComponent } from "./question-component";
5
- import { api } from "../lib/demo-api";
6
-
7
- export const PreviewSettingsArea = ({
8
- questionType,
9
- question,
10
- setQuestion,
11
- }) => {
12
- return (
13
- <div
14
- data-type="question"
15
- data-question-id={question.id}
16
- className={cn(
17
- "rounded-xl border border-border bg-main text-black",
18
- "mb-3 relative bg-white transition-[border,shadow,opacity] p-6 px-7",
19
- )}
20
- >
21
- <div className="flex gap-3">
22
- <div
23
- className={cn(
24
- "size-6 rounded-full border-2 items-center justify-center flex transition bg-bright",
25
- )}
26
- >
27
- {questionType?.icon ? (
28
- <img
29
- src={`/api/icon?path=${questionType.icon}&questionType=${questionType.id}`}
30
- alt={questionType.name}
31
- className="size-3.5 object-contain"
32
- />
33
- ) : (
34
- <FileQuestionIcon className="size-3.5" strokeWidth={2.6} />
35
- )}
36
- </div>
37
- <div className="flex flex-1">
38
- <div className="flex-1 flex flex-col">
39
- <MinimalRichTextField
40
- singleLine
41
- readOnly
42
- className={cn(
43
- "w-full font-medium",
44
- "cursor-not-allowed pointer-events-none",
45
- )}
46
- value={question.title}
47
- placeholder={questionType?.titlePlaceholder}
48
- />
49
- <MinimalRichTextField
50
- data-type="description"
51
- className={cn(
52
- "w-full text-sm mt-1",
53
- "cursor-not-allowed pointer-events-none",
54
- )}
55
- value={question.description}
56
- readOnly
57
- showFormattingMenu
58
- placeholder={questionType?.descriptionPlaceholder}
59
- />
60
- </div>
61
- </div>
62
- </div>
63
- <div className="border-t border-border pt-5 mt-5 ml-9 mb-4">
64
- <div>
65
- <QuestionComponent
66
- type={questionType}
67
- componentName="settings-area"
68
- props={{
69
- api,
70
- question,
71
- settings: question.settings,
72
- environment: "exam",
73
- setMultipleSettings: (newSettings) => {
74
- setQuestion({
75
- ...question,
76
- settings: {
77
- ...question.settings,
78
- ...newSettings,
79
- },
80
- });
81
- },
82
- setSetting: (settingId, value) => {
83
- setQuestion({
84
- ...question,
85
- settings: {
86
- ...question.settings,
87
- [settingId]: value,
88
- },
89
- });
90
- },
91
- }}
92
- />
93
- </div>
94
- </div>
95
- </div>
96
- );
97
- };
@@ -1,75 +0,0 @@
1
- import { ComponentType } from "react";
2
- import { ErrorBoundary, FallbackProps } from "react-error-boundary";
3
- import { useTranslation } from "react-i18next";
4
-
5
- import {
6
- fetchComponent,
7
- FormattedQuestionType,
8
- FrontendBaseComponentProps,
9
- } from "@examplary/ui";
10
- import { Loader2Icon } from "lucide-react";
11
- import useSWR from "swr";
12
-
13
- type QuestionComponentProps = {
14
- type: FormattedQuestionType;
15
- componentName: "assessment" | "print" | "results" | "settings-area";
16
- props: Omit<FrontendBaseComponentProps, "api" | "t" | "i18n"> &
17
- Record<string, any>;
18
- };
19
-
20
- export const QuestionComponentContent = ({
21
- type,
22
- componentName,
23
- props,
24
- }: QuestionComponentProps) => {
25
- const componentUrl = type?.components?.[componentName];
26
-
27
- const { t, i18n } = useTranslation(type?.translationNs);
28
- const { data: Component, error } = useSWR<ComponentType<any>>(
29
- componentUrl,
30
- fetchComponent,
31
- {
32
- revalidateOnFocus: false,
33
- revalidateOnReconnect: false,
34
- },
35
- );
36
-
37
- if (!componentUrl) {
38
- return null;
39
- }
40
-
41
- if (error) {
42
- return <FallbackComponent error={error} />;
43
- }
44
-
45
- if (!Component) {
46
- return <Loader2Icon className="animate-spin size-5" />;
47
- }
48
-
49
- const finalProps = {
50
- t,
51
- i18n,
52
- ...props,
53
- };
54
-
55
- return <Component {...finalProps} />;
56
- };
57
-
58
- const FallbackComponent = ({ error }: Partial<FallbackProps>) => {
59
- return (
60
- <div className="rounded-md bg-gray-100 p-5">
61
- <p className="text-red-500 text-sm font-semibold">
62
- Failed loading question component
63
- </p>
64
- <pre className="text-sm font-mono">{error?.message}</pre>
65
- </div>
66
- );
67
- };
68
-
69
- export const QuestionComponent = (props: QuestionComponentProps) => {
70
- return (
71
- <ErrorBoundary FallbackComponent={FallbackComponent}>
72
- <QuestionComponentContent {...props} />
73
- </ErrorBoundary>
74
- );
75
- };
@@ -1,11 +0,0 @@
1
- @layer modules_theme, theme, modules_base, base, modules_components, components, modules_utilities, utilities;
2
-
3
- @import "tailwindcss";
4
- @import "@examplary/ui/src/global.css";
5
-
6
- @layer base {
7
- :root {
8
- --radius: 0.5rem;
9
- --font-sans: "DM Sans", "Inter", "sans-serif";
10
- }
11
- }
@@ -1,29 +0,0 @@
1
- export const api = {
2
- get: async (path: string, options: any) => {
3
- console.warn("API calls are not supported in the preview UI");
4
- return {};
5
- },
6
- post: async (path: string, body: any, options: any) => {
7
- console.warn("API calls are not supported in the preview UI");
8
- return {};
9
- },
10
- put: async (path: string, body: any, options: any) => {
11
- console.warn("API calls are not supported in the preview UI");
12
- return {};
13
- },
14
- delete: async (path: string, options: any) => {
15
- console.warn("API calls are not supported in the preview UI");
16
- return {};
17
- },
18
- patch: async (path: string, body: any, options: any) => {
19
- console.warn("API calls are not supported in the preview UI");
20
- return {};
21
- },
22
- uploadFile: async (accept: "*/*") => {
23
- console.warn("File uploads are not supported in the preview UI");
24
- return {
25
- url: `https://placehold.co/600x400?${Math.random()}`,
26
- name: "image.jpg",
27
- };
28
- },
29
- };
@@ -1,15 +0,0 @@
1
- import { initReactI18next } from "react-i18next";
2
-
3
- import I18n from "i18next";
4
- import LanguageDetector from "i18next-browser-languagedetector";
5
-
6
- // eslint-disable-next-line react-hooks/rules-of-hooks
7
- export const i18n = I18n.use(initReactI18next)
8
- .use(LanguageDetector)
9
- .init({
10
- interpolation: {
11
- escapeValue: false,
12
- },
13
- supportedLngs: ["en", "nl"],
14
- fallbackLng: "en",
15
- });
@@ -1,8 +0,0 @@
1
- import { createRoot } from "react-dom/client";
2
-
3
- import App from "./App.tsx";
4
- import "./lib/i18n.ts";
5
-
6
- import "./index.css";
7
-
8
- createRoot(document.getElementById("root")!).render(<App />);