@octolearn/training-ui 0.0.0-stage → 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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -3
  3. package/THIRD-PARTY-NOTICES.md +211 -0
  4. package/dist/chunks/core-rJUDTpGS.js +592 -0
  5. package/dist/chunks/define-elements-CeSpZJXj.js +6111 -0
  6. package/dist/chunks/font-data-DlFp9eSH.js +8 -0
  7. package/dist/core.js +11 -0
  8. package/dist/define.js +52 -0
  9. package/dist/index.js +50 -0
  10. package/dist/octolearn-training-ui.bundle.js +6441 -0
  11. package/dist/types/base-element.d.ts +35 -0
  12. package/dist/types/content.d.ts +51 -0
  13. package/dist/types/core.d.ts +13 -0
  14. package/dist/types/d-icons.d.ts +10 -0
  15. package/dist/types/define-elements.d.ts +18 -0
  16. package/dist/types/define.d.ts +1 -0
  17. package/dist/types/diff.d.ts +34 -0
  18. package/dist/types/elements/arithmetic-question.d.ts +31 -0
  19. package/dist/types/elements/card.d.ts +43 -0
  20. package/dist/types/elements/choice-pair.d.ts +21 -0
  21. package/dist/types/elements/dictation-question.d.ts +51 -0
  22. package/dist/types/elements/feedback-toast.d.ts +29 -0
  23. package/dist/types/elements/feedback.d.ts +87 -0
  24. package/dist/types/elements/flashcard.d.ts +46 -0
  25. package/dist/types/elements/matching-round.d.ts +61 -0
  26. package/dist/types/elements/multiple-choice.d.ts +33 -0
  27. package/dist/types/elements/picture-question.d.ts +30 -0
  28. package/dist/types/elements/progress.d.ts +24 -0
  29. package/dist/types/elements/question-answer.d.ts +20 -0
  30. package/dist/types/elements/question-parts.d.ts +88 -0
  31. package/dist/types/elements/question.d.ts +37 -0
  32. package/dist/types/elements/text-answer.d.ts +57 -0
  33. package/dist/types/elements/tile-answer.d.ts +43 -0
  34. package/dist/types/elements/today-stats.d.ts +17 -0
  35. package/dist/types/elements/trainer-overlay.d.ts +45 -0
  36. package/dist/types/elements/trainer-panel.d.ts +46 -0
  37. package/dist/types/elements/trainer.d.ts +57 -0
  38. package/dist/types/elements/true-false.d.ts +37 -0
  39. package/dist/types/events.d.ts +53 -0
  40. package/dist/types/feedback-message.d.ts +34 -0
  41. package/dist/types/font-data.d.ts +2 -0
  42. package/dist/types/fonts.d.ts +9 -0
  43. package/dist/types/i18n/strings.d.ts +233 -0
  44. package/dist/types/icons.d.ts +21 -0
  45. package/dist/types/images.d.ts +2 -0
  46. package/dist/types/index.d.ts +32 -0
  47. package/dist/types/keys.d.ts +6 -0
  48. package/dist/types/session/third-party-transport.d.ts +22 -0
  49. package/dist/types/session/training-session.d.ts +167 -0
  50. package/dist/types/session/transport.d.ts +44 -0
  51. package/dist/types/speech.d.ts +71 -0
  52. package/dist/types/styles/base.d.ts +9 -0
  53. package/dist/types/styles/d-parts.d.ts +13 -0
  54. package/dist/types/styles/tokens.d.ts +95 -0
  55. package/dist/types/viewport.d.ts +35 -0
  56. package/package.json +67 -6
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 octolearn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,413 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @octolearn/training-ui
2
+
3
+ Octolearn's training components as web components: the question card of octolearn's design D in every answer format,
4
+ the feedback after an answer, today's statistic, and two ready-made placements for games: an overlay while the game is
5
+ paused, and a side panel while it keeps running. A `TrainingSession` runs the learning queue against octolearn's API.
6
+
7
+ The components work in any web page or browser game, whatever framework it uses (React, Vue, Angular, Svelte or none),
8
+ and look the same everywhere: their styles live in a shadow root that the page's CSS cannot reach. Octolearn's own
9
+ training page is built from them.
10
+
11
+ - [Install](#install)
12
+ - [Quick start: a question in your game](#quick-start-a-question-in-your-game)
13
+ - [Talking to octolearn](#talking-to-octolearn)
14
+ - [Placements](#placements)
15
+ - [TrainingSession](#trainingsession)
16
+ - [The elements](#the-elements)
17
+ - [Events](#events)
18
+ - [Language, texts and speech](#language-texts-and-speech)
19
+ - [Theming](#theming)
20
+ - [Frameworks](#frameworks)
21
+ - [Accessibility](#accessibility)
22
+ - [Browser support, CSP and size](#browser-support-csp-and-size)
23
+ - [License](#license)
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ npm install @octolearn/training-ui
29
+ ```
30
+
31
+ Register the elements once, before (or after) they are used. Elements already in the page upgrade when they are
32
+ defined.
33
+
34
+ ```js
35
+ import { defineTrainingElements } from '@octolearn/training-ui';
36
+
37
+ defineTrainingElements(); // or: import '@octolearn/training-ui/define';
38
+ ```
39
+
40
+ `defineTrainingElements({ fonts: false })` skips loading the fonts (Montserrat and Nothing You Could Do, bundled, about
41
+ 70 kB, loaded lazily); the components then fall back to the page's Montserrat or a sans-serif. Tag names that are already
42
+ registered are skipped, so two copies of the package on one page do not throw.
43
+
44
+ Without a build step, use the bundle from a CDN. It contains Lit and registers everything itself:
45
+
46
+ ```html
47
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@octolearn/training-ui@0.1.0/dist/octolearn-training-ui.bundle.js"></script>
48
+ ```
49
+
50
+ | Entry | Contents |
51
+ | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `@octolearn/training-ui` | everything: elements, `defineTrainingElements`, session, transports, speech, types |
53
+ | `@octolearn/training-ui/define` | the same, and registers the elements on import |
54
+ | `@octolearn/training-ui/core` | without Lit and without elements: session, transports, `SpeechController`, types (for code that drives the elements, server code, test runners) |
55
+ | `…/dist/octolearn-training-ui.bundle.js` | one self-contained module for a `<script type="module">` tag |
56
+
57
+ Lit is a dependency of the package; bundlers share it with an app that uses Lit itself.
58
+
59
+ ## Quick start: a question in your game
60
+
61
+ The game pauses, an overlay asks a question, the learner answers, the feedback appears in the card, and "Weiter" (or
62
+ Escape) goes back to the game:
63
+
64
+ ```html
65
+ <octolearn-trainer-overlay id="overlay"></octolearn-trainer-overlay>
66
+
67
+ <script type="module">
68
+ import { defineTrainingElements, TrainingSession, createThirdPartyTransport } from '@octolearn/training-ui';
69
+
70
+ defineTrainingElements();
71
+
72
+ const session = new TrainingSession(
73
+ createThirdPartyTransport({
74
+ baseUrl: '/octolearn', // your server, see "Talking to octolearn"
75
+ headers: () => ({ Authorization: `Bearer ${learnerToken}` }), // from the device code flow
76
+ }),
77
+ { holdForFeedback: true }, // the feedback stays in the card until "Weiter"
78
+ );
79
+
80
+ const overlay = document.getElementById('overlay');
81
+ overlay.session = session;
82
+ overlay.addEventListener('octolearn-close', () => overlay.close()); // Escape: may the learner skip? Your call.
83
+
84
+ // reward a right answer; after "Weiter" the game goes on
85
+ let answered = false;
86
+ session.addEventListener('answered', event => {
87
+ answered = true;
88
+ if (event.detail.correct) {
89
+ myGame.reward();
90
+ }
91
+ });
92
+ session.addEventListener('change', () => {
93
+ if (answered && session.state !== 'feedback') {
94
+ answered = false;
95
+ overlay.close();
96
+ myGame.resume();
97
+ }
98
+ });
99
+
100
+ await session.start();
101
+ overlay.show(); // e.g. when the player loses a life: pause your game, then show the question
102
+ </script>
103
+ ```
104
+
105
+ The session is all you need to keep: it fetches questions ahead, sends the answers, counts the day and knows what is on
106
+ screen.
107
+
108
+ ## Talking to octolearn
109
+
110
+ Games use octolearn's third-party API (see the [integration guide](https://octolearn.de/docs)). It needs the game's
111
+ `apiKey` and `apiSecret` on every request, and **the secret must never reach the browser**. So the browser talks to
112
+ _your_ server, which adds the two headers and forwards the call:
113
+
114
+ ```js
115
+ // Express: /octolearn/* → https://app.octolearn.de/api/third-party/*, with the game's credentials added
116
+ app.use('/octolearn/third-party', express.json(), async (req, res) => {
117
+ const path = req.url === '/' ? '' : req.url; // PUT /octolearn/third-party → PUT /api/third-party (no trailing slash)
118
+ const response = await fetch(`https://app.octolearn.de/api/third-party${path}`, {
119
+ method: req.method,
120
+ headers: {
121
+ Accept: 'application/json',
122
+ 'Content-Type': 'application/json',
123
+ Authorization: req.headers.authorization ?? '', // the learner's token from the device code flow
124
+ apiKey: process.env.OCTOLEARN_API_KEY,
125
+ apiSecret: process.env.OCTOLEARN_API_SECRET,
126
+ },
127
+ body: req.method === 'GET' ? undefined : JSON.stringify(req.body),
128
+ });
129
+ for (const header of ['X-Rate-Limit-Remaining', 'X-Rate-Limit-Retry-After-Seconds']) {
130
+ const value = response.headers.get(header);
131
+ if (value) res.set(header, value);
132
+ }
133
+ res
134
+ .status(response.status)
135
+ .type('application/json')
136
+ .send(await response.text());
137
+ });
138
+ ```
139
+
140
+ `createThirdPartyTransport(options)` then calls `/octolearn/third-party/next-relevant-questions`, `PUT /octolearn/third-party`
141
+ and `/octolearn/third-party/get-todays-statistic`:
142
+
143
+ | Option | Default | Meaning |
144
+ | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------- |
145
+ | `baseUrl` | – | where octolearn's `/api` is reached through your server, e.g. `'/octolearn'` |
146
+ | `headers` | none | a function returning headers for every request (may be async), typically `{ Authorization: 'Bearer …' }` |
147
+ | `batchSize` | `5` | questions per request |
148
+ | `queryFormats` | the API's default | answer formats to ask for; without it a game gets only `MultipleChoice` and `TextInput` |
149
+ | `learningModes` | all | only these learning modes (`QuestionAnswer`, `Dictation`, `Arithmetic`, `Picture`) |
150
+ | `fetch` | global `fetch` | your own fetch |
151
+
152
+ A request that fails with an error status rejects with a `TransportError` (`status`, and `retryAfterSeconds` on a 429).
153
+
154
+ To show the newer formats in a game, name them: `queryFormats: ['MultipleChoice', 'TextInput', 'LetterTiles', 'WordTiles', 'Fragment', 'TrueFalse']`.
155
+ The Karteikarte (`Flashcard`) and the rounds of "Paare finden" exist only in octolearn's web app.
156
+
157
+ For any other backend, implement the `Transport` interface yourself: `nextQuestions()`, `submitAnswer(content, answer)`,
158
+ and optionally `todaysStatistic()`, `matchingRound()` and `revealFlashcard(content)`.
159
+
160
+ ## Placements
161
+
162
+ All three take a `session` and do the rest; without one they show their children instead.
163
+
164
+ ### `<octolearn-trainer-overlay>`: the game pauses
165
+
166
+ A modal dialog over the page: the game stays visible behind a dark, blurred layer with the note "Spiel pausiert", the
167
+ card sits in the middle. The page behind is inert, so the keyboard and screen readers stay in the dialog; when it closes,
168
+ the focus goes back to where it was.
169
+
170
+ | Property / attribute | Meaning |
171
+ | ------------------------------- | ----------------------------------------------------------------------- |
172
+ | `open` / `show()` / `close()` | shows and hides the dialog |
173
+ | `session` | the TrainingSession to show |
174
+ | `status` | the note over the game (default "Spiel pausiert"); `status=""` hides it |
175
+ | `no-autospeak`, `no-keys` | passed on (see `<octolearn-question>`) |
176
+ | CSS `--octolearn-overlay-width` | width of the card (default `760px`) |
177
+
178
+ Escape fires `octolearn-close` and **leaves the overlay open**: the game decides whether the learner may skip the
179
+ question.
180
+
181
+ ### `<octolearn-trainer-panel>`: the game keeps running
182
+
183
+ Design D's side panel, 400 px wide by default (size it like any block element): the octolearn logo, "Lernfrage" and a
184
+ star counter; the question card; the game's reward; Inkie saying how many questions are left in the round.
185
+
186
+ ```html
187
+ <octolearn-trainer-panel no-keys no-autofocus>
188
+ <p slot="reward"><strong>Belohnung</strong> Richtig = 1 Extraleben im Spiel.</p>
189
+ </octolearn-trainer-panel>
190
+ ```
191
+
192
+ | Property / attribute | Meaning |
193
+ | ------------------------- | ------------------------------------------------------------ |
194
+ | `session` | the TrainingSession to show |
195
+ | `stars` | the star counter; default: the session's right answers today |
196
+ | `message` | Inkie's line; default: "Noch 2 Fragen in dieser Runde!" |
197
+ | `no-keys`, `no-autofocus` | leave the keyboard to the running game |
198
+ | slot `reward` | what the game gives for a right answer (hidden while empty) |
199
+
200
+ ### `<octolearn-trainer>`: just the card
201
+
202
+ The card driven by a session, for your own layout: the question in any format, the feedback with "Weiter", rounds of
203
+ "Paare finden", and messages while loading, after an error ("Erneut versuchen"), when everything due is done
204
+ ("Freiwillig weiterüben") and when nothing is left. Attributes: `session`, `settings` (show "Einstellungen" in a
205
+ dictation), `no-autospeak`, `no-autofocus`, `no-keys`, `compact` (no header, as in the panel), `flat` (a lighter shadow).
206
+ Methods: `focusAnswer()`, `focusSettings()`.
207
+
208
+ Don't call `session.answer()` yourself as well: the trainer does.
209
+
210
+ ## TrainingSession
211
+
212
+ ```js
213
+ const session = new TrainingSession(transport, { holdForFeedback: true });
214
+ session.addEventListener('change', redraw);
215
+ await session.start();
216
+ ```
217
+
218
+ The order rules are octolearn's: the next questions are fetched before the last one is answered; a refill leaves out
219
+ questions still on screen; a question answered a moment ago goes behind the others of a batch, but a wrongly answered one
220
+ does come back in the same session; with a transport that offers rounds, a round of "Paare finden" comes at the start and
221
+ after every 8 answers.
222
+
223
+ | Option | Default | Meaning |
224
+ | ----------------- | ------- | ------------------------------------------------------------------------------- |
225
+ | `holdForFeedback` | `false` | stay in state `feedback` after an answer until `continue()` (design D) |
226
+ | `prefetchAt` | `2` | fetch the next questions when this many are left |
227
+ | `recentMemory` | `3` | how many answered questions are kept back from the very next turn |
228
+ | `roundSize` | `5` | questions per round, for the stars |
229
+ | `matchingEvery` | `8` | a round of "Paare finden" after this many answers (only with `matchingRound()`) |
230
+
231
+ `state` is one of `idle`, `loading`, `question` (`current` is the question), `feedback` (`feedback` describes the
232
+ answer), `pairs` (`round` is a round of "Paare finden"), `due-done` (only practice is left: offer `startPractising()`),
233
+ `done` and `error` (`reload()` tries again).
234
+
235
+ | Member | Meaning |
236
+ | ------------------------------------------- | ---------------------------------------------------------------------------------------------- |
237
+ | `start()`, `reload()` | load the first questions (and today's counts), or again after an error |
238
+ | `answer(text, { dontKnow })` | send the answer to `current`; returns the `AnswerOutcome` |
239
+ | `continue()` | leave the feedback ("Weiter") |
240
+ | `checkPair(item, answer)`, `finishRound()` | check one pair of a round; go on after it |
241
+ | `revealFlashcard(content)` | the answer of a Karteikarte (web app only) |
242
+ | `startPractising()` | go on with practice after everything due |
243
+ | `correct`, `wrong`, `streak` | today's counts and right answers in a row |
244
+ | `roundProgress` | `{ current, total, results }` for the stars |
245
+ | events `change`, `answered`, `pair-checked` | redraw; an `AnswerOutcome` (with `feedback` ready for `<octolearn-feedback>`); a `PairOutcome` |
246
+
247
+ The server decides whether an answer is right; the package never sees the solution before the answer is sent.
248
+
249
+ ## The elements
250
+
251
+ Questions take `content`, a question exactly as octolearn's API sends it (`FrontendContent`). User content is always
252
+ shown as text, never as HTML.
253
+
254
+ | Element | What it is | Main properties / attributes |
255
+ | --------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
256
+ | `<octolearn-question>` | any question: picks the element by answer format, then learning mode | `content`, `settings`, `no-autospeak`, `no-autofocus`, `no-keys`, `revealFlashcard` |
257
+ | `<octolearn-question-answer>` | Vokabel / question, also Stimmt das? and Karteikarte | `content`, `no-autofocus`, `no-keys`, `revealFlashcard` |
258
+ | `<octolearn-dictation-question>` | Diktat: listen, "Langsam anhören", optional "Einstellungen" | `content`, `settings`, `no-autospeak`, `no-autofocus`, `no-keys` |
259
+ | `<octolearn-arithmetic-question>` | Rechnen ("7 · 8 =") | `content`, `no-autofocus`, `no-keys` |
260
+ | `<octolearn-picture-question>` | Bilderrätsel | `content`, `no-autofocus`, `no-keys` |
261
+ | `<octolearn-multiple-choice>` | answer tiles, keys 1–9 | `options`, `label`, `no-keys` |
262
+ | `<octolearn-text-answer>` | the big field, "Weiß ich nicht", Wortfragment hint | `label`, `visible-label`, `type`, `input-lang`, `hint`, `no-dont-know`, `value` |
263
+ | `<octolearn-tile-answer>` | Buchstabensalat / Satzpuzzle | `tiles`, `separator` (`""` letters, `" "` words) |
264
+ | `<octolearn-true-false>` | Stimmt das? (← / →) | `statement`, `statement-lang`, `label`, `no-keys` |
265
+ | `<octolearn-flashcard>` | Karteikarte: "Antwort zeigen", then "Gewusst" / "Nicht gewusst" | `content`, `reveal` (a function returning the answer), `no-keys` |
266
+ | `<octolearn-matching-round>` | Paare finden | `round`, `checkPair` |
267
+ | `<octolearn-feedback>` | the right / wrong panel with "Weiter"; right answers continue after 3 s | `message` (an `AnswerOutcome.feedback`), `no-auto`, `delay`, `compact` |
268
+ | `<octolearn-feedback-toast>` | a small toast for pages that keep their own layout | `show(message)`, `clear()` |
269
+ | `<octolearn-card>` | the card: band, header with label, chip and stars, Inkie | `state` (`question` / `right` / `wrong`), `label`, `chip`, `current`, `total`, `results`, `no-inkie`, `no-header`, `flat` |
270
+ | `<octolearn-progress>` | the stars "Frage 3 von 5" | `current`, `total`, `results` |
271
+ | `<octolearn-today-stats>` | today's right and wrong answers | `correct`, `wrong` |
272
+
273
+ Without the session, you send the answers yourself: listen to `octolearn-answer` and call octolearn (or your backend);
274
+ show the outcome with `<octolearn-feedback>` in the card.
275
+
276
+ What each format sends as `answer`: the chosen option (multiple choice), the typed text (text and Wortfragment), the
277
+ tiles joined without separator (letters) or with single spaces (words), `"true"` / `"false"` (Stimmt das?),
278
+ `"known"` / `"unknown"` (Karteikarte), and `"?"` with `dontKnow: true` for "Weiß ich nicht".
279
+
280
+ ## Events
281
+
282
+ All events bubble and cross shadow roots (`composed`), so one listener on a parent element or `document` hears them.
283
+
284
+ | Event | From | `detail` |
285
+ | ---------------------- | ------------------------- | ------------------------------------------------------------------------- |
286
+ | `octolearn-answer` | every answer element | `{ answer, dontKnow? }` |
287
+ | `octolearn-speak` | questions, feedback | `{ text, reason, rate?, lang? }`; cancelable: cancel it to speak yourself |
288
+ | `octolearn-stop` | dictation | – |
289
+ | `octolearn-settings` | dictation with `settings` | – ("Einstellungen" was pressed) |
290
+ | `octolearn-continue` | feedback | – ("Weiter", or the 3 s after a right answer) |
291
+ | `octolearn-pair` | matching round | `{ item, answer, correct, correctAnswer }` |
292
+ | `octolearn-round-done` | matching round | – ("Weiter" after the last pair) |
293
+ | `octolearn-close` | overlay | – (Escape) |
294
+
295
+ Keys are only taken while nothing else uses them: not while the learner types in a field, not when the element is
296
+ hidden, and never with `no-keys` (for a game that needs 1–9 or the arrow keys itself).
297
+
298
+ ## Language, texts and speech
299
+
300
+ The texts are German (`de`) or English (`en`), following the `lang` attribute of the element or the nearest ancestor
301
+ (else the page). Any text can be replaced through the `strings` property (`Partial<TrainingStrings>`; the defaults are
302
+ exported as `de` and `en`).
303
+
304
+ Dictations are read aloud with the browser's speech synthesis; the voice and speed the learner chose are remembered
305
+ (`getSpeechController().choose(voice, rate)`). A word is only read by a voice of its language (`detail.lang`: the
306
+ content's `sourceLanguage`, else `de`): the learner's voice if it speaks that language, else Google Deutsch or the first
307
+ voice of it. Without one the browser gets the language alone, and the dictation shows a note that the device has no
308
+ voice for it (`getSpeechController().hasVoiceFor(lang)`). If the browser blocks speech before the first tap, the
309
+ question shows an "Abspielen" button. To use your own audio, cancel `octolearn-speak` and play `detail.text` in
310
+ `detail.lang` yourself, or pass both to `getSpeechController().speak(text, { lang, rate })`, which waits for the
311
+ browser's voice list (Chrome's first `getVoices()` is often empty).
312
+
313
+ ## Theming
314
+
315
+ The look is octolearn's design D and meant to stay recognisable. What can be adjusted, through CSS custom properties on
316
+ the elements or any ancestor:
317
+
318
+ | Property | Default |
319
+ | ------------------------------------------------------------------- | -------------------------------- |
320
+ | `--octolearn-font-body` | Montserrat (bundled) |
321
+ | `--octolearn-font-headline` | Nothing You Could Do (bundled) |
322
+ | `--octolearn-d-accent` | `#f1ab30` (buttons, band, stars) |
323
+ | `--octolearn-d-on-accent` | `#333333` (text on the accent) |
324
+ | `--octolearn-d-ink`, `--octolearn-d-muted` | `#333333`, `#5f5f5f` |
325
+ | `--octolearn-d-paper`, `--octolearn-d-cream`, `--octolearn-d-panel` | `#ffffff`, `#fdf0d7`, `#f2f2f2` |
326
+ | `--octolearn-d-right`, `--octolearn-d-wrong` (and `-bg`, `-text`) | `#2f9e5b`, `#d64545` |
327
+ | `--octolearn-d-backdrop` | the overlay's shade |
328
+ | `--octolearn-overlay-width` | `760px` |
329
+
330
+ For dark pages, set `theme="dark"` on the element you place (`<octolearn-trainer-overlay theme="dark">`, a panel, a
331
+ question …); the elements inside it follow. The card becomes dark lined paper with light text, the orange accent stays
332
+ and keeps dark text on it, and the right/wrong panels get dark backgrounds. Text reaches at least 4.5:1, borders and the
333
+ focus ring at least 3:1. The dark values are set on the element itself, so to adjust them, set the properties on the
334
+ same element or inside it, for example `octolearn-trainer-panel[theme='dark'] { --octolearn-d-panel: #000; }`. Content
335
+ you slot into the panel inherits the properties too, so `color: var(--octolearn-d-label, #9a5f00)` follows the theme.
336
+
337
+ Sizes are in px on purpose: a page with a different root font size gets the same component. Below 560 px of width the components
338
+ switch to their phone sizes (container queries, so a narrow side panel counts too).
339
+
340
+ ## Frameworks
341
+
342
+ The elements are standard custom elements: set **properties** (objects, arrays, functions) and listen to **events**.
343
+
344
+ ```jsx
345
+ // React 19 sets properties on custom elements; the event goes through a ref
346
+ function Question({ question, onAnswer }) {
347
+ const ref = useRef(null);
348
+ useEffect(() => {
349
+ const element = ref.current;
350
+ element.addEventListener('octolearn-answer', onAnswer);
351
+ return () => element.removeEventListener('octolearn-answer', onAnswer);
352
+ }, [onAnswer]);
353
+ return <octolearn-question ref={ref} content={question} />;
354
+ }
355
+ ```
356
+
357
+ With React 18, set `content` through the ref as well (`ref.current.content = question` in an effect): React 18 passes
358
+ everything to custom elements as attributes.
359
+
360
+ ```vue
361
+ <!-- Vue 3: vite.config → vue({ template: { compilerOptions: { isCustomElement: tag => tag.startsWith('octolearn-') } } }) -->
362
+ <octolearn-question :content.prop="question" @octolearn-answer="onAnswer" />
363
+ ```
364
+
365
+ ```html
366
+ <!-- Angular: schemas: [CUSTOM_ELEMENTS_SCHEMA] in the component -->
367
+ <octolearn-question [content]="question" (octolearn-answer)="onAnswer($event)"></octolearn-question>
368
+ ```
369
+
370
+ Svelte and plain JavaScript need nothing special. With TypeScript the tag names, properties and events are typed
371
+ (`HTMLElementTagNameMap`, `HTMLElementEventMap`) once the package is imported.
372
+
373
+ ## Accessibility
374
+
375
+ Every control is a native button or field with a visible label and a 4 px focus ring; targets are at least 44 px.
376
+ Feedback is announced (`role="status"`), the overlay is a real modal dialog, the matching round and the tiles work
377
+ without dragging, the progress stars say "Frage 3 von 5, richtig", and the hints about keyboard shortcuts are hidden
378
+ on touch screens. Right answers move on after 3 s; the timer pauses while the pointer rests on the card, and `no-auto` turns it
379
+ off. Reduced motion is respected.
380
+
381
+ ## Phones and the on-screen keyboard
382
+
383
+ When a phone's keyboard opens it covers about half of the screen. The elements notice this through
384
+ `window.visualViewport` (CSS can't: the layout viewport keeps its size) and set the reflected attribute `short` on
385
+ themselves while less than 520 px (`SHORT_VIEWPORT_PX`) are visible. In that state the card hides its header and Inkie
386
+ (the label and stars stay for screen readers), texts, pictures and buttons get smaller, the answer field carries a
387
+ round send button instead of "Fertig", and "Weiß ich nicht" becomes a small text button. A question whose field has the
388
+ focus then scrolls its card to the top of the visible part, so the word or picture stays above the field. When the
389
+ keyboard closes, everything is back as before. The page can follow the same state, e.g. with
390
+ `.layout:has(octolearn-trainer[short])` or `getViewportWatcher().subscribe(short => …)`.
391
+
392
+ ## Browser support, CSP and size
393
+
394
+ Current Chrome, Edge, Firefox and Safari (custom elements, shadow DOM, container queries: Chrome 105+, Firefox 110+,
395
+ Safari 16+). With a Content-Security-Policy, allow `img-src data:` and `font-src data:` (the logo, Inkie and the fonts are
396
+ bundled as data URLs); no inline scripts or styles are needed. The package is about 40 kB gzipped plus the fonts, which
397
+ load lazily.
398
+
399
+ ## License
400
+
401
+ MIT, see [LICENSE](./LICENSE). Not covered by it: the octolearn logo and Inkie, octolearn's mascot, are © octolearn and
402
+ may be shown only as part of these components. The bundled fonts (SIL Open Font License) and icons are listed in
403
+ [THIRD-PARTY-NOTICES.md](./THIRD-PARTY-NOTICES.md).
404
+
405
+ ## Development
406
+
407
+ ```sh
408
+ npm run build -w @octolearn/training-ui # dist/: ES modules, type declarations, CDN bundle
409
+ npm run test -w @octolearn/training-ui # type check + Vitest (happy-dom)
410
+ npm run dev -w @octolearn/training-ui # demo playground: /demo/, /demo/compare.html, /demo/game.html
411
+ ```
412
+
413
+ Publishing a release: see `PUBLISHING.md` next to this file in the octolearn repository.
@@ -0,0 +1,211 @@
1
+ # Third-party notices
2
+
3
+ @octolearn/training-ui itself is MIT licensed (see LICENSE). Its build contains the following works that are not under the MIT license.
4
+
5
+ ## octolearn logo and Inkie
6
+
7
+ The octolearn logo (`src/assets/logo.svg`) and the octopus Inkie (`src/assets/inkie.svg`) are © octolearn and are not covered by the MIT license. They may be shown, unchanged, as part of these components. Using them anywhere else, or changed, needs octolearn's permission.
8
+
9
+ ## Font Awesome Free icons
10
+
11
+ Icons `check`, `divide`, `dumbbell`, `gear`, `headphones`, `minus`, `play`, `plus` and `xmark` from Font Awesome Free 6.7.2 by Fonticons, Inc. (https://fontawesome.com), licensed under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). The icon path data is unchanged.
12
+
13
+ ## Montserrat
14
+
15
+ Montserrat, latin subset, variable weight 100–900 (via @fontsource-variable/montserrat 5.3.0), licensed under the SIL Open Font License 1.1. It is embedded under the family name "Octolearn Montserrat".
16
+
17
+ ```text
18
+ Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat) Montserrat-Italic[wght].ttf: Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)
19
+
20
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
21
+ This license is copied below, and is also available with a FAQ at:
22
+ http://scripts.sil.org/OFL
23
+
24
+
25
+ -----------------------------------------------------------
26
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
27
+ -----------------------------------------------------------
28
+
29
+ PREAMBLE
30
+ The goals of the Open Font License (OFL) are to stimulate worldwide
31
+ development of collaborative font projects, to support the font creation
32
+ efforts of academic and linguistic communities, and to provide a free and
33
+ open framework in which fonts may be shared and improved in partnership
34
+ with others.
35
+
36
+ The OFL allows the licensed fonts to be used, studied, modified and
37
+ redistributed freely as long as they are not sold by themselves. The
38
+ fonts, including any derivative works, can be bundled, embedded,
39
+ redistributed and/or sold with any software provided that any reserved
40
+ names are not used by derivative works. The fonts and derivatives,
41
+ however, cannot be released under any other type of license. The
42
+ requirement for fonts to remain under this license does not apply
43
+ to any document created using the fonts or their derivatives.
44
+
45
+ DEFINITIONS
46
+ "Font Software" refers to the set of files released by the Copyright
47
+ Holder(s) under this license and clearly marked as such. This may
48
+ include source files, build scripts and documentation.
49
+
50
+ "Reserved Font Name" refers to any names specified as such after the
51
+ copyright statement(s).
52
+
53
+ "Original Version" refers to the collection of Font Software components as
54
+ distributed by the Copyright Holder(s).
55
+
56
+ "Modified Version" refers to any derivative made by adding to, deleting,
57
+ or substituting -- in part or in whole -- any of the components of the
58
+ Original Version, by changing formats or by porting the Font Software to a
59
+ new environment.
60
+
61
+ "Author" refers to any designer, engineer, programmer, technical
62
+ writer or other person who contributed to the Font Software.
63
+
64
+ PERMISSION & CONDITIONS
65
+ Permission is hereby granted, free of charge, to any person obtaining
66
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
67
+ redistribute, and sell modified and unmodified copies of the Font
68
+ Software, subject to the following conditions:
69
+
70
+ 1) Neither the Font Software nor any of its individual components,
71
+ in Original or Modified Versions, may be sold by itself.
72
+
73
+ 2) Original or Modified Versions of the Font Software may be bundled,
74
+ redistributed and/or sold with any software, provided that each copy
75
+ contains the above copyright notice and this license. These can be
76
+ included either as stand-alone text files, human-readable headers or
77
+ in the appropriate machine-readable metadata fields within text or
78
+ binary files as long as those fields can be easily viewed by the user.
79
+
80
+ 3) No Modified Version of the Font Software may use the Reserved Font
81
+ Name(s) unless explicit written permission is granted by the corresponding
82
+ Copyright Holder. This restriction only applies to the primary font name as
83
+ presented to the users.
84
+
85
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
86
+ Software shall not be used to promote, endorse or advertise any
87
+ Modified Version, except to acknowledge the contribution(s) of the
88
+ Copyright Holder(s) and the Author(s) or with their explicit written
89
+ permission.
90
+
91
+ 5) The Font Software, modified or unmodified, in part or in whole,
92
+ must be distributed entirely under this license, and must not be
93
+ distributed under any other license. The requirement for fonts to
94
+ remain under this license does not apply to any document created
95
+ using the Font Software.
96
+
97
+ TERMINATION
98
+ This license becomes null and void if any of the above conditions are
99
+ not met.
100
+
101
+ DISCLAIMER
102
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
103
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
104
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
105
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
106
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
107
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
108
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
109
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
110
+ OTHER DEALINGS IN THE FONT SOFTWARE.
111
+ ```
112
+
113
+ ## Nothing You Could Do
114
+
115
+ Nothing You Could Do, latin subset, regular weight (via @fontsource/nothing-you-could-do 5.3.0), licensed under the SIL Open Font License 1.1. It is embedded under the family name "Octolearn Headline".
116
+
117
+ ```text
118
+ Copyright (c) 2010, Kimberly Geswein (kimberlygeswein.com kimberlygeswein@gmail.com). This Font Software is licensed under the SIL Open Font License, Version 1.1. This license available with a FAQ at: http://scripts.sil.org/OFL
119
+
120
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
121
+ This license is copied below, and is also available with a FAQ at:
122
+ http://scripts.sil.org/OFL
123
+
124
+
125
+ -----------------------------------------------------------
126
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
127
+ -----------------------------------------------------------
128
+
129
+ PREAMBLE
130
+ The goals of the Open Font License (OFL) are to stimulate worldwide
131
+ development of collaborative font projects, to support the font creation
132
+ efforts of academic and linguistic communities, and to provide a free and
133
+ open framework in which fonts may be shared and improved in partnership
134
+ with others.
135
+
136
+ The OFL allows the licensed fonts to be used, studied, modified and
137
+ redistributed freely as long as they are not sold by themselves. The
138
+ fonts, including any derivative works, can be bundled, embedded,
139
+ redistributed and/or sold with any software provided that any reserved
140
+ names are not used by derivative works. The fonts and derivatives,
141
+ however, cannot be released under any other type of license. The
142
+ requirement for fonts to remain under this license does not apply
143
+ to any document created using the fonts or their derivatives.
144
+
145
+ DEFINITIONS
146
+ "Font Software" refers to the set of files released by the Copyright
147
+ Holder(s) under this license and clearly marked as such. This may
148
+ include source files, build scripts and documentation.
149
+
150
+ "Reserved Font Name" refers to any names specified as such after the
151
+ copyright statement(s).
152
+
153
+ "Original Version" refers to the collection of Font Software components as
154
+ distributed by the Copyright Holder(s).
155
+
156
+ "Modified Version" refers to any derivative made by adding to, deleting,
157
+ or substituting -- in part or in whole -- any of the components of the
158
+ Original Version, by changing formats or by porting the Font Software to a
159
+ new environment.
160
+
161
+ "Author" refers to any designer, engineer, programmer, technical
162
+ writer or other person who contributed to the Font Software.
163
+
164
+ PERMISSION & CONDITIONS
165
+ Permission is hereby granted, free of charge, to any person obtaining
166
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
167
+ redistribute, and sell modified and unmodified copies of the Font
168
+ Software, subject to the following conditions:
169
+
170
+ 1) Neither the Font Software nor any of its individual components,
171
+ in Original or Modified Versions, may be sold by itself.
172
+
173
+ 2) Original or Modified Versions of the Font Software may be bundled,
174
+ redistributed and/or sold with any software, provided that each copy
175
+ contains the above copyright notice and this license. These can be
176
+ included either as stand-alone text files, human-readable headers or
177
+ in the appropriate machine-readable metadata fields within text or
178
+ binary files as long as those fields can be easily viewed by the user.
179
+
180
+ 3) No Modified Version of the Font Software may use the Reserved Font
181
+ Name(s) unless explicit written permission is granted by the corresponding
182
+ Copyright Holder. This restriction only applies to the primary font name as
183
+ presented to the users.
184
+
185
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
186
+ Software shall not be used to promote, endorse or advertise any
187
+ Modified Version, except to acknowledge the contribution(s) of the
188
+ Copyright Holder(s) and the Author(s) or with their explicit written
189
+ permission.
190
+
191
+ 5) The Font Software, modified or unmodified, in part or in whole,
192
+ must be distributed entirely under this license, and must not be
193
+ distributed under any other license. The requirement for fonts to
194
+ remain under this license does not apply to any document created
195
+ using the Font Software.
196
+
197
+ TERMINATION
198
+ This license becomes null and void if any of the above conditions are
199
+ not met.
200
+
201
+ DISCLAIMER
202
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
203
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
204
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
205
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
206
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
207
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
208
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
209
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
210
+ OTHER DEALINGS IN THE FONT SOFTWARE.
211
+ ```