@octolearn/training-ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +397 -3
- package/THIRD-PARTY-NOTICES.md +211 -0
- package/dist/chunks/core-DR1lxtEg.js +562 -0
- package/dist/chunks/define-elements-BKBBQkbv.js +5515 -0
- package/dist/chunks/font-data-DlFp9eSH.js +8 -0
- package/dist/core.js +11 -0
- package/dist/define.js +49 -0
- package/dist/index.js +47 -0
- package/dist/octolearn-training-ui.bundle.js +5893 -0
- package/dist/types/base-element.d.ts +28 -0
- package/dist/types/content.d.ts +51 -0
- package/dist/types/core.d.ts +13 -0
- package/dist/types/d-icons.d.ts +10 -0
- package/dist/types/define-elements.d.ts +18 -0
- package/dist/types/define.d.ts +1 -0
- package/dist/types/diff.d.ts +34 -0
- package/dist/types/elements/arithmetic-question.d.ts +31 -0
- package/dist/types/elements/card.d.ts +42 -0
- package/dist/types/elements/choice-pair.d.ts +21 -0
- package/dist/types/elements/dictation-question.d.ts +42 -0
- package/dist/types/elements/feedback-toast.d.ts +29 -0
- package/dist/types/elements/feedback.d.ts +87 -0
- package/dist/types/elements/flashcard.d.ts +46 -0
- package/dist/types/elements/matching-round.d.ts +61 -0
- package/dist/types/elements/multiple-choice.d.ts +33 -0
- package/dist/types/elements/picture-question.d.ts +18 -0
- package/dist/types/elements/progress.d.ts +24 -0
- package/dist/types/elements/question-answer.d.ts +22 -0
- package/dist/types/elements/question-parts.d.ts +80 -0
- package/dist/types/elements/question.d.ts +37 -0
- package/dist/types/elements/text-answer.d.ts +50 -0
- package/dist/types/elements/tile-answer.d.ts +43 -0
- package/dist/types/elements/today-stats.d.ts +17 -0
- package/dist/types/elements/trainer-overlay.d.ts +45 -0
- package/dist/types/elements/trainer-panel.d.ts +46 -0
- package/dist/types/elements/trainer.d.ts +57 -0
- package/dist/types/elements/true-false.d.ts +37 -0
- package/dist/types/events.d.ts +53 -0
- package/dist/types/feedback-message.d.ts +34 -0
- package/dist/types/font-data.d.ts +2 -0
- package/dist/types/fonts.d.ts +9 -0
- package/dist/types/i18n/strings.d.ts +228 -0
- package/dist/types/icons.d.ts +21 -0
- package/dist/types/images.d.ts +2 -0
- package/dist/types/index.d.ts +31 -0
- package/dist/types/keys.d.ts +6 -0
- package/dist/types/session/third-party-transport.d.ts +22 -0
- package/dist/types/session/training-session.d.ts +167 -0
- package/dist/types/session/transport.d.ts +44 -0
- package/dist/types/speech.d.ts +54 -0
- package/dist/types/styles/base.d.ts +9 -0
- package/dist/types/styles/d-parts.d.ts +12 -0
- package/dist/types/styles/tokens.d.ts +95 -0
- 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,397 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
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-keys` |
|
|
259
|
+
| `<octolearn-arithmetic-question>` | Rechnen ("7 · 8 =") | `content`, `no-keys` |
|
|
260
|
+
| `<octolearn-picture-question>` | Bilderrätsel | `content`, `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)`). If the browser blocks speech before the first tap, the question shows an
|
|
306
|
+
"Abspielen" button. To use your own audio, cancel `octolearn-speak` and play `detail.text` yourself.
|
|
307
|
+
|
|
308
|
+
## Theming
|
|
309
|
+
|
|
310
|
+
The look is octolearn's design D and meant to stay recognisable. What can be adjusted, through CSS custom properties on
|
|
311
|
+
the elements or any ancestor:
|
|
312
|
+
|
|
313
|
+
| Property | Default |
|
|
314
|
+
| ------------------------------------------------------------------- | -------------------------------- |
|
|
315
|
+
| `--octolearn-font-body` | Montserrat (bundled) |
|
|
316
|
+
| `--octolearn-font-headline` | Nothing You Could Do (bundled) |
|
|
317
|
+
| `--octolearn-d-accent` | `#f1ab30` (buttons, band, stars) |
|
|
318
|
+
| `--octolearn-d-on-accent` | `#333333` (text on the accent) |
|
|
319
|
+
| `--octolearn-d-ink`, `--octolearn-d-muted` | `#333333`, `#5f5f5f` |
|
|
320
|
+
| `--octolearn-d-paper`, `--octolearn-d-cream`, `--octolearn-d-panel` | `#ffffff`, `#fdf0d7`, `#f2f2f2` |
|
|
321
|
+
| `--octolearn-d-right`, `--octolearn-d-wrong` (and `-bg`, `-text`) | `#2f9e5b`, `#d64545` |
|
|
322
|
+
| `--octolearn-d-backdrop` | the overlay's shade |
|
|
323
|
+
| `--octolearn-overlay-width` | `760px` |
|
|
324
|
+
|
|
325
|
+
For dark pages, set `theme="dark"` on the element you place (`<octolearn-trainer-overlay theme="dark">`, a panel, a
|
|
326
|
+
question …); the elements inside it follow. The card becomes dark lined paper with light text, the orange accent stays
|
|
327
|
+
and keeps dark text on it, and the right/wrong panels get dark backgrounds. Text reaches at least 4.5:1, borders and the
|
|
328
|
+
focus ring at least 3:1. The dark values are set on the element itself, so to adjust them, set the properties on the
|
|
329
|
+
same element or inside it, for example `octolearn-trainer-panel[theme='dark'] { --octolearn-d-panel: #000; }`. Content
|
|
330
|
+
you slot into the panel inherits the properties too, so `color: var(--octolearn-d-label, #9a5f00)` follows the theme.
|
|
331
|
+
|
|
332
|
+
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
|
|
333
|
+
switch to their phone sizes (container queries, so a narrow side panel counts too).
|
|
334
|
+
|
|
335
|
+
## Frameworks
|
|
336
|
+
|
|
337
|
+
The elements are standard custom elements: set **properties** (objects, arrays, functions) and listen to **events**.
|
|
338
|
+
|
|
339
|
+
```jsx
|
|
340
|
+
// React 19 sets properties on custom elements; the event goes through a ref
|
|
341
|
+
function Question({ question, onAnswer }) {
|
|
342
|
+
const ref = useRef(null);
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
const element = ref.current;
|
|
345
|
+
element.addEventListener('octolearn-answer', onAnswer);
|
|
346
|
+
return () => element.removeEventListener('octolearn-answer', onAnswer);
|
|
347
|
+
}, [onAnswer]);
|
|
348
|
+
return <octolearn-question ref={ref} content={question} />;
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
With React 18, set `content` through the ref as well (`ref.current.content = question` in an effect): React 18 passes
|
|
353
|
+
everything to custom elements as attributes.
|
|
354
|
+
|
|
355
|
+
```vue
|
|
356
|
+
<!-- Vue 3: vite.config → vue({ template: { compilerOptions: { isCustomElement: tag => tag.startsWith('octolearn-') } } }) -->
|
|
357
|
+
<octolearn-question :content.prop="question" @octolearn-answer="onAnswer" />
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
```html
|
|
361
|
+
<!-- Angular: schemas: [CUSTOM_ELEMENTS_SCHEMA] in the component -->
|
|
362
|
+
<octolearn-question [content]="question" (octolearn-answer)="onAnswer($event)"></octolearn-question>
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Svelte and plain JavaScript need nothing special. With TypeScript the tag names, properties and events are typed
|
|
366
|
+
(`HTMLElementTagNameMap`, `HTMLElementEventMap`) once the package is imported.
|
|
367
|
+
|
|
368
|
+
## Accessibility
|
|
369
|
+
|
|
370
|
+
Every control is a native button or field with a visible label and a 4 px focus ring; targets are at least 44 px.
|
|
371
|
+
Feedback is announced (`role="status"`), the overlay is a real modal dialog, the matching round and the tiles work
|
|
372
|
+
without dragging, the progress stars say "Frage 3 von 5, richtig", and the hints about keyboard shortcuts are hidden
|
|
373
|
+
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
|
|
374
|
+
off. Reduced motion is respected.
|
|
375
|
+
|
|
376
|
+
## Browser support, CSP and size
|
|
377
|
+
|
|
378
|
+
Current Chrome, Edge, Firefox and Safari (custom elements, shadow DOM, container queries: Chrome 105+, Firefox 110+,
|
|
379
|
+
Safari 16+). With a Content-Security-Policy, allow `img-src data:` and `font-src data:` (the logo, Inkie and the fonts are
|
|
380
|
+
bundled as data URLs); no inline scripts or styles are needed. The package is about 40 kB gzipped plus the fonts, which
|
|
381
|
+
load lazily.
|
|
382
|
+
|
|
383
|
+
## License
|
|
384
|
+
|
|
385
|
+
MIT, see [LICENSE](./LICENSE). Not covered by it: the octolearn logo and Inkie, octolearn's mascot, are © octolearn and
|
|
386
|
+
may be shown only as part of these components. The bundled fonts (SIL Open Font License) and icons are listed in
|
|
387
|
+
[THIRD-PARTY-NOTICES.md](./THIRD-PARTY-NOTICES.md).
|
|
388
|
+
|
|
389
|
+
## Development
|
|
390
|
+
|
|
391
|
+
```sh
|
|
392
|
+
npm run build -w @octolearn/training-ui # dist/: ES modules, type declarations, CDN bundle
|
|
393
|
+
npm run test -w @octolearn/training-ui # type check + Vitest (happy-dom)
|
|
394
|
+
npm run dev -w @octolearn/training-ui # demo playground: /demo/, /demo/compare.html, /demo/game.html
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
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
|
+
```
|