@lernapps/app-templates 0.1.0 → 0.2.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 (55) hide show
  1. package/README.md +2 -2
  2. package/dist/{assessment-By2GGTJC.mjs → assessment-Dy359O1O.mjs} +1 -1
  3. package/dist/{blocks-BmRrD7UY.mjs → blocks-CxtCPAa5.mjs} +2 -2
  4. package/dist/blocks.mjs +2 -2
  5. package/dist/{check-BkEl-m-F.mjs → check-CteIFN96.mjs} +1 -1
  6. package/dist/{content-cYBPYg_J.mjs → content-DO6oEJxj.mjs} +1 -1
  7. package/dist/{e2e-BiMI-pCL.mjs → e2e-CU0n_unV.mjs} +1 -1
  8. package/dist/{engine-BqF2Vffn.mjs → engine-CArxqizW.mjs} +1 -1
  9. package/dist/explainer-e2e.mjs +54 -19
  10. package/dist/explainer-plugin.mjs +6 -5
  11. package/dist/explainer.mjs +4 -4
  12. package/dist/interactive-e2e.mjs +1 -1
  13. package/dist/interactive-plugin.mjs +2 -2
  14. package/dist/{load-C-0hUaSC.mjs → load-RosvaPZf.mjs} +37 -8
  15. package/dist/questions-e2e.mjs +1 -1
  16. package/dist/questions-engine.mjs +2 -2
  17. package/dist/questions.mjs +2 -2
  18. package/dist/{quiz-BKc8JYtC.mjs → quiz-DnW-xtCh.mjs} +1 -1
  19. package/dist/quiz-e2e.mjs +52 -28
  20. package/dist/quiz-plugin.mjs +4 -4
  21. package/dist/quiz.mjs +3 -3
  22. package/dist/{render-BL6fV_NI.mjs → render-D5kwkNt1.mjs} +2 -2
  23. package/dist/{render-DpRO_RKm.mjs → render-DqHOJyHK.mjs} +3 -3
  24. package/dist/{render-BJey0bwl.mjs → render-GS1Htlu8.mjs} +9 -4
  25. package/dist/{result-_eQcamoz.mjs → result-D_mmCVwN.mjs} +2 -2
  26. package/dist/{validate-COaZso7u.mjs → validate-BDwL7Su3.mjs} +6 -2
  27. package/dist/{validate-CxtdeQ5S.mjs → validate-Bxy9NfB8.mjs} +1 -1
  28. package/docs/arc42/09-decisions.arc42.md +24 -0
  29. package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +7 -3
  30. package/explainer/e2e/runtime.e2e.ts +6 -0
  31. package/explainer/package.json +2 -1
  32. package/interactive/package.json +2 -1
  33. package/package.json +38 -2
  34. package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +7 -3
  35. package/quiz/e2e/runtime.e2e.ts +5 -0
  36. package/quiz/package.json +2 -1
  37. package/runtime/explainer/assessment-page.ts +2 -0
  38. package/runtime/explainer/e2e.ts +204 -149
  39. package/runtime/explainer/exercises.ts +2 -0
  40. package/runtime/explainer/outline.ts +8 -1
  41. package/runtime/quiz/e2e.ts +172 -135
  42. package/runtime/shared/questions/question.ts +3 -0
  43. package/runtime/shared/questions/question.v1.schema.json +4 -0
  44. package/runtime/shared/questions/render.ts +14 -6
  45. package/runtime/shared/text/markdown.ts +1 -1
  46. package/runtime/shared/text/speech.ts +39 -0
  47. package/scaffold/explainer/src/app.yaml +5 -0
  48. package/scaffold/explainer/src/thema/chapter.yaml +4 -0
  49. package/scaffold/explainer/src/thema/einstieg.yaml +28 -0
  50. package/scaffold/interactive/src/app.yaml +8 -0
  51. package/scaffold/interactive/src/core/index.ts +23 -0
  52. package/scaffold/interactive/src/messages/de.json +6 -0
  53. package/scaffold/interactive/src/playthroughs/zaehlen.yaml +12 -0
  54. package/scaffold/interactive/src/ui/App.vue +27 -0
  55. package/scaffold/quiz/src/quiz.json +31 -0
package/dist/quiz.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { S as shuffled, g as questionId, u as isCorrect } from "./render-BJey0bwl.mjs";
2
- import { n as startQuestion } from "./engine-BqF2Vffn.mjs";
3
- import { a as de_default, i as en_default, r as solutionId } from "./quiz-BKc8JYtC.mjs";
1
+ import { S as shuffled, g as questionId, u as isCorrect } from "./render-GS1Htlu8.mjs";
2
+ import { n as startQuestion } from "./engine-CArxqizW.mjs";
3
+ import { a as de_default, i as en_default, r as solutionId } from "./quiz-DnW-xtCh.mjs";
4
4
  import { translator } from "@lernapps/tooling/i18n";
5
5
  import { announce, moveFocus } from "@lernapps/tooling/a11y";
6
6
  import { createStorage } from "@lernapps/tooling/storage";
@@ -1,7 +1,7 @@
1
1
  import { i as inLanguage, l as textsIn, o as languageListProblems, r as LANGUAGES } from "./html-mffkHGHh.mjs";
2
- import { a as plain, s as renderMarkdown } from "./check-BkEl-m-F.mjs";
2
+ import { c as renderMarkdown, o as plain } from "./check-CteIFN96.mjs";
3
3
  import { a as pageFooter, o as pageHeader, r as languageRoot, s as relative, t as alternateLinks } from "./pages-9RaaomoO.mjs";
4
- import { c as schemaProblems, n as Texts, s as readYaml, t as Problems } from "./content-cYBPYg_J.mjs";
4
+ import { c as schemaProblems, n as Texts, s as readYaml, t as Problems } from "./content-DO6oEJxj.mjs";
5
5
  import { Ajv2020 } from "ajv/dist/2020.js";
6
6
  import { existsSync, readdirSync } from "node:fs";
7
7
  import { join } from "node:path";
@@ -1,7 +1,7 @@
1
1
  import { l as textsIn, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
2
- import { n as renderMaterial } from "./blocks-BmRrD7UY.mjs";
3
- import { a as renderQuestion, g as questionId, o as renderSolution, r as external, t as QUESTION_MESSAGES } from "./render-BJey0bwl.mjs";
4
- import { a as de_default, i as en_default, r as solutionId } from "./quiz-BKc8JYtC.mjs";
2
+ import { n as renderMaterial } from "./blocks-CxtCPAa5.mjs";
3
+ import { a as renderQuestion, g as questionId, o as renderSolution, r as external, t as QUESTION_MESSAGES } from "./render-GS1Htlu8.mjs";
4
+ import { a as de_default, i as en_default, r as solutionId } from "./quiz-DnW-xtCh.mjs";
5
5
  import { a as pageFooter, o as pageHeader, r as languageRoot, s as relative, t as alternateLinks } from "./pages-9RaaomoO.mjs";
6
6
  import { translator } from "@lernapps/tooling/i18n";
7
7
  // runtime/quiz/render.ts
@@ -199,6 +199,7 @@ function questionIn(question, lang) {
199
199
  text: t(option.text),
200
200
  correct: option.correct,
201
201
  info: t(option.info),
202
+ ...option.alt === void 0 ? {} : { alt: t(option.alt) },
202
203
  ...option.link === void 0 ? {} : { link: link(option.link) }
203
204
  }))
204
205
  };
@@ -415,13 +416,17 @@ function questionWords(lang = "de") {
415
416
  }
416
417
  return found;
417
418
  }
418
- function controls(question, { t }, format) {
419
+ function controls(question, { t }, format, describe) {
419
420
  const name = questionId(question);
420
421
  switch (question.type) {
421
422
  case "single-choice":
422
423
  case "multiple-choice": {
423
424
  const type = question.type === "single-choice" ? "radio" : "checkbox";
424
- return question.options.map((option, index) => `<label class="question-option"><input type="${type}" name="${name}" value="${index}"> <span>${format(option.text)}</span></label>`).join("\n");
425
+ return question.options.map((option, index) => {
426
+ const spoken = option.alt ?? describe?.(option.text);
427
+ const label = spoken === void 0 ? "" : ` aria-label="${escape(spoken)}"`;
428
+ return `<label class="question-option"><input type="${type}" name="${name}" value="${index}"${label}> <span>${format(option.text)}</span></label>`;
429
+ }).join("\n");
425
430
  }
426
431
  case "true-false": return [true, false].map((value) => `<label class="question-option"><input type="radio" name="${name}" value="${value}"> <span>${t(value ? "true" : "false")}</span></label>`).join("\n");
427
432
  case "number": return [
@@ -456,7 +461,7 @@ function controls(question, { t }, format) {
456
461
  * what the learner learns about it (the same as its solution). The engine's startQuestion makes it live; without
457
462
  * JavaScript the button stays hidden.
458
463
  */
459
- function renderQuestion(question, { element = "div", next = false, lang = "de", textHtml, materialHtml, explanationHtml, format = escape } = {}) {
464
+ function renderQuestion(question, { element = "div", next = false, lang = "de", textHtml, materialHtml, explanationHtml, format = escape, describe } = {}) {
460
465
  const words = questionWords(lang);
461
466
  const { t } = words;
462
467
  const id = questionId(question);
@@ -467,7 +472,7 @@ function renderQuestion(question, { element = "div", next = false, lang = "de",
467
472
  `<fieldset>`,
468
473
  `<legend class="question-text">${textHtml ?? format(question.text)}</legend>`,
469
474
  ...materialHtml === void 0 || materialHtml === "" ? [] : [`<div class="question-material">${materialHtml}</div>`],
470
- controls(question, words, format),
475
+ controls(question, words, format, describe),
471
476
  `</fieldset>`,
472
477
  `<div class="question-feedback" role="status"></div>`,
473
478
  `<div class="question-reveal" hidden>`,
@@ -1,5 +1,5 @@
1
- import { x as rightEntries } from "./render-BJey0bwl.mjs";
2
- import { w as message } from "./assessment-By2GGTJC.mjs";
1
+ import { x as rightEntries } from "./render-GS1Htlu8.mjs";
2
+ import { w as message } from "./assessment-Dy359O1O.mjs";
3
3
  // runtime/explainer/result.ts
4
4
  const key = (id, n) => n === void 0 ? id : `${id}=${n}`;
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  import { c as textLanguages } from "./html-mffkHGHh.mjs";
2
- import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-CxtdeQ5S.mjs";
3
- import { _ as questionIn, h as questionAddresses, l as isComplete, m as parseNumber, u as isCorrect, v as questionTexts, x as rightEntries } from "./render-BJey0bwl.mjs";
2
+ import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-Bxy9NfB8.mjs";
3
+ import { _ as questionIn, h as questionAddresses, l as isComplete, m as parseNumber, u as isCorrect, v as questionTexts, x as rightEntries } from "./render-GS1Htlu8.mjs";
4
4
  import { Ajv2020 } from "ajv/dist/2020.js";
5
5
  // runtime/shared/examples.ts
6
6
  /**
@@ -591,6 +591,10 @@ var question_v1_schema_default = {
591
591
  "description": "One to three sentences of background on this option itself, true whether or not it is the right answer, e.g. \"Der Uluru ist ein Inselberg in der Wüste Zentralaustraliens ...\". After an answer it shows for every option. For a wrong option it is also the reason a learner who chose it reads with the feedback, so say there why it does not fit.",
592
592
  "$ref": "urn:lernapps:schema:text.v1#/$defs/text"
593
593
  },
594
+ "alt": {
595
+ "description": "Optional: the option as it is spoken, for the name of its radio button or checkbox, when its text is only a formula, e.g. \"zwei hoch sieben\" for \"$2^7$\". Without it the name is made from the formula in the page's language (\"2 hoch 7\"); write it where that reads badly, e.g. for a chemical formula. Not needed when the text has words.",
596
+ "$ref": "urn:lernapps:schema:text.v1#/$defs/text"
597
+ },
594
598
  "link": {
595
599
  "description": "Further reading on this option.",
596
600
  "$ref": "#/$defs/link"
@@ -1,6 +1,6 @@
1
1
  import { i as inLanguage } from "./html-mffkHGHh.mjs";
2
2
  import { n as markerOf } from "./block-BK_ohsH1.mjs";
3
- import { t as checkText } from "./check-BkEl-m-F.mjs";
3
+ import { t as checkText } from "./check-CteIFN96.mjs";
4
4
  // runtime/shared/blocks/validate.ts
5
5
  /** The definition of each type of block in the schema of the blocks (blocks.v1.schema.json). */
6
6
  const MATERIAL_DEFINITIONS = {
@@ -228,6 +228,30 @@ addresses: qg-understandable
228
228
  :::
229
229
  ```
230
230
 
231
+ ## A New App Gets the Scaffold, Not the Example
232
+
233
+ `lernapps create` copies only the scaffold of the archetype into a new app: the paths that `lernapps.scaffold.<archetype>`
234
+ in this package's `package.json` lists (the entry, the configuration, the e2e entry) and a placeholder as start content
235
+ (`scaffold/<archetype>/`: one question, one page with one exercise, or a counter as the core). The template folder
236
+ stays a complete example app, which this repo checks; an app does not copy it, the creator's assistant looks it up in
237
+ `node_modules/@lernapps/app-templates/<archetype>/`. A copied example shaped the new app too much: its structure,
238
+ its topic and its tone stayed long after the plan said otherwise. The list is data in this package, which alone knows
239
+ what is configuration and what is example; the tooling only reads it. The CI job `scaffold` checks the scaffold as an
240
+ app, so that a new app passes `lernapps check` from its first commit. `lernapps update` never writes the scaffold
241
+ again: it moves libraries, not content. Rejected: a function `scaffold()` exported by the package, which would need the
242
+ built package even in a clone given to `create --templates`; start content inside the template folder, which the
243
+ template's own checks would read as part of the example.
244
+
245
+ ```arc42
246
+ :::decision
247
+ id: dec-scaffold
248
+ title: A new app gets the scaffold and a placeholder, not the example app
249
+ status: accepted
250
+ date: 2026-11-02
251
+ addresses: qg-understandable
252
+ :::
253
+ ```
254
+
231
255
  ## Tutor Variant
232
256
 
233
257
  The optional tutor is a handoff to the learner's own assistant: after a click, a prompt (copied or passed to a chosen
@@ -488,8 +488,12 @@ path: runtime/explainer/assessment.ts
488
488
 
489
489
  ### End-to-end tests
490
490
 
491
- Built. `explainerTests()` derives from the content what every page must show, checks it without JavaScript in every
492
- language, and with JavaScript one exercise per type and page and every component.
491
+ Built, in two parts. `explainerTests()` checks the app's content: it derives from the content what every page must
492
+ show and checks it without JavaScript in every language, previous and next, and with JavaScript the app's own
493
+ components and the checks of its exercise components. `explainerRuntimeTests()` checks the runtime, the same in every
494
+ app: one exercise per type and page, the videos, the tutor ways, the start page, the self-assessments, what is stored.
495
+ Only this repo runs the second part, on the template and its fixtures (`explainer/e2e/runtime.e2e.ts`, not in the
496
+ scaffold).
493
497
 
494
498
  ```arc42
495
499
  :::building-block
@@ -504,7 +508,7 @@ requires: if-archetype-content, if-runtime-texts, if-questions
504
508
 
505
509
  #### Tests interface
506
510
 
507
- The app's `e2e/explainer.e2e.ts` only calls `explainerTests()`.
511
+ The app's `e2e/explainer.e2e.ts`, part of the scaffold, only calls `explainerTests()`.
508
512
 
509
513
  ```arc42
510
514
  :::interface
@@ -0,0 +1,6 @@
1
+ import { explainerRuntimeTests } from "@lernapps/app-templates/explainer/e2e";
2
+
3
+ // The tests of the explainer runtime, which this repo runs on the template and its fixtures. Not part of the scaffold
4
+ // (lernapps.scaffold in the package.json of @lernapps/app-templates): a new app runs only explainerTests(), on its
5
+ // content.
6
+ explainerRuntimeTests();
@@ -9,10 +9,11 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "@lernapps/app-templates": "file:..",
12
- "@lernapps/tooling": "github:lernapps/tooling#7354ffe7ae7d8079ba5f714d5b402666d27df066",
12
+ "@lernapps/tooling": "^0.1.2",
13
13
  "vite-plus": "1.4.0"
14
14
  },
15
15
  "overrides": {
16
+ "@lernapps/app-templates": "file:..",
16
17
  "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0"
17
18
  }
18
19
  }
@@ -9,10 +9,11 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "@lernapps/app-templates": "file:..",
12
- "@lernapps/tooling": "github:lernapps/tooling#7354ffe7ae7d8079ba5f714d5b402666d27df066",
12
+ "@lernapps/tooling": "^0.1.2",
13
13
  "vite-plus": "1.4.0"
14
14
  },
15
15
  "overrides": {
16
+ "@lernapps/app-templates": "file:..",
16
17
  "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0"
17
18
  }
18
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lernapps/app-templates",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The templates of the archetypes of lernapps.net apps, and the runtime of each archetype that apps depend on (the quiz: engine, build step, styles, end-to-end tests, the schema of the question bank, the questions it shares with the explainer are in ./questions; the explainer: build step, client module with its exercises, the islands of its Vue components, styles, end-to-end tests, the schema of its content; the interactive archetype: build step, the start page and the app page around the app's own Vue components and core, its playthroughs, styles, end-to-end tests, the schemas of its start page and its playthroughs)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,6 +14,7 @@
14
14
  "explainer",
15
15
  "interactive",
16
16
  "quiz",
17
+ "scaffold",
17
18
  "!**/package-lock.json",
18
19
  "!**/node_modules"
19
20
  ],
@@ -100,6 +101,7 @@
100
101
  "highlight.js": "11.12.0",
101
102
  "marked": "18.1.2",
102
103
  "source-map-js": "1.2.2",
104
+ "speech-rule-engine": "4.1.5",
103
105
  "temml": "0.14.0",
104
106
  "vue": "3.5.44",
105
107
  "yaml": "2.9.1"
@@ -141,6 +143,40 @@
141
143
  "explainer",
142
144
  "interactive",
143
145
  "quiz"
144
- ]
146
+ ],
147
+ "scaffold": {
148
+ "explainer": [
149
+ ".gitignore",
150
+ "index.html",
151
+ "package.json",
152
+ "tsconfig.json",
153
+ "vite.config.ts",
154
+ "playwright.config.ts",
155
+ "src/main.ts",
156
+ "e2e/explainer.e2e.ts"
157
+ ],
158
+ "interactive": [
159
+ ".arc42ignore",
160
+ ".gitignore",
161
+ "index.html",
162
+ "package.json",
163
+ "tsconfig.json",
164
+ "vite.config.ts",
165
+ "playwright.config.ts",
166
+ "src/main.ts",
167
+ "src/env.d.ts",
168
+ "e2e/interactive.e2e.ts"
169
+ ],
170
+ "quiz": [
171
+ ".gitignore",
172
+ "index.html",
173
+ "package.json",
174
+ "tsconfig.json",
175
+ "vite.config.ts",
176
+ "playwright.config.ts",
177
+ "src/main.ts",
178
+ "e2e/quiz.e2e.ts"
179
+ ]
180
+ }
145
181
  }
146
182
  }
@@ -196,8 +196,12 @@ path: runtime/quiz/engine.ts
196
196
 
197
197
  ### End-to-end tests
198
198
 
199
- Built. `quizTests()` checks every question of the bank in the browser (deep link, right and wrong answer, feedback in
200
- view with the focus), the full score, the page without JavaScript, and each further language.
199
+ Built, in two parts. `quizTests()` checks the app's content: each question's deep link with what it shows, the page
200
+ without JavaScript with every question and its solution, and each further language. `quizRuntimeTests()` checks the
201
+ runtime, the same in every app: right and wrong answer with the feedback in view and the focus, the seed and the order,
202
+ the run to the full score, the number of questions, what is stored. Only this repo runs the second part, on the
203
+ template and its fixtures (`quiz/e2e/runtime.e2e.ts`, not in the scaffold): its time grows with the bank, and it tests
204
+ nothing an app writes.
201
205
 
202
206
  ```arc42
203
207
  :::building-block
@@ -212,7 +216,7 @@ requires: if-archetype-content, if-runtime-texts
212
216
 
213
217
  #### Tests interface
214
218
 
215
- The app's `e2e/quiz.e2e.ts` only calls `quizTests()`.
219
+ The app's `e2e/quiz.e2e.ts`, part of the scaffold, only calls `quizTests()`.
216
220
 
217
221
  ```arc42
218
222
  :::interface
@@ -0,0 +1,5 @@
1
+ import { quizRuntimeTests } from "@lernapps/app-templates/quiz/e2e";
2
+
3
+ // The tests of the quiz runtime, which this repo runs on the template and its fixtures. Not part of the scaffold
4
+ // (lernapps.scaffold in the package.json of @lernapps/app-templates): a new app runs only quizTests(), on its content.
5
+ quizRuntimeTests();
package/quiz/package.json CHANGED
@@ -9,10 +9,11 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "@lernapps/app-templates": "file:..",
12
- "@lernapps/tooling": "github:lernapps/tooling#7354ffe7ae7d8079ba5f714d5b402666d27df066",
12
+ "@lernapps/tooling": "^0.1.2",
13
13
  "vite-plus": "1.4.0"
14
14
  },
15
15
  "overrides": {
16
+ "@lernapps/app-templates": "file:..",
16
17
  "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0"
17
18
  }
18
19
  }
@@ -12,6 +12,7 @@
12
12
  import { renderQuestion } from "../shared/questions/render.ts";
13
13
  import { type MaterialContext, renderMaterial } from "../shared/blocks/index.ts";
14
14
  import { plain, renderWithFormulas } from "../shared/text/markdown.ts";
15
+ import { formulaAlternative } from "../shared/text/speech.ts";
15
16
  import {
16
17
  assessments,
17
18
  drawsTasks,
@@ -117,6 +118,7 @@ function itemHtml(context: AssessmentContext, data: AssessmentItemData, n: numbe
117
118
  return `${label}${renderQuestion(question, {
118
119
  lang,
119
120
  format: (text) => rendered[text] ?? renderWithFormulas(text),
121
+ describe: (text) => formulaAlternative(text, lang),
120
122
  ...(question.material === undefined ? {} : { materialHtml: renderMaterial(question.material, context.material) }),
121
123
  })}`;
122
124
  }