@octolearn/training-ui 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.
- package/README.md +21 -5
- package/dist/chunks/{core-DR1lxtEg.js → core-rJUDTpGS.js} +49 -19
- package/dist/chunks/{define-elements-BKBBQkbv.js → define-elements-CeSpZJXj.js} +661 -65
- package/dist/core.js +1 -1
- package/dist/define.js +13 -10
- package/dist/index.js +12 -9
- package/dist/octolearn-training-ui.bundle.js +1592 -1044
- package/dist/types/base-element.d.ts +7 -0
- package/dist/types/d-icons.d.ts +1 -1
- package/dist/types/elements/card.d.ts +2 -1
- package/dist/types/elements/dictation-question.d.ts +11 -2
- package/dist/types/elements/picture-question.d.ts +14 -2
- package/dist/types/elements/question-answer.d.ts +0 -2
- package/dist/types/elements/question-parts.d.ts +11 -3
- package/dist/types/elements/text-answer.d.ts +7 -0
- package/dist/types/i18n/strings.d.ts +5 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/speech.d.ts +19 -2
- package/dist/types/styles/d-parts.d.ts +2 -1
- package/dist/types/viewport.d.ts +35 -0
- package/package.json +1 -1
|
@@ -8,6 +8,8 @@ import { type TrainingLanguage, type TrainingStrings } from './i18n/strings';
|
|
|
8
8
|
* changes; to switch the language at runtime, set it on the element.
|
|
9
9
|
* - `strings` (property): replaces single texts, e.g. `{ check: 'Prüfen' }`.
|
|
10
10
|
* - `theme` (attribute): `dark` for dark surfaces.
|
|
11
|
+
* - `short` (reflected attribute, set by the element itself): the visible area is short, e.g. a phone's on-screen keyboard
|
|
12
|
+
* covers half of it. The elements then use a compact layout (`:host([short])` in their styles).
|
|
11
13
|
*
|
|
12
14
|
* Templates start with `<div class="root" lang=…>` (see baseStyles), which also tells screen readers the language.
|
|
13
15
|
*/
|
|
@@ -18,6 +20,11 @@ export declare class OctolearnElement extends LitElement {
|
|
|
18
20
|
language?: string;
|
|
19
21
|
strings?: Partial<TrainingStrings>;
|
|
20
22
|
theme?: 'light' | 'dark';
|
|
23
|
+
/** The visible area is short (on-screen keyboard open): compact layout. Follows `window.visualViewport`. */
|
|
24
|
+
short: boolean;
|
|
25
|
+
private endViewportWatch?;
|
|
26
|
+
connectedCallback(): void;
|
|
27
|
+
disconnectedCallback(): void;
|
|
21
28
|
protected get resolvedLanguage(): TrainingLanguage;
|
|
22
29
|
/** The texts in the element's language, with the page's overrides. */
|
|
23
30
|
protected get t(): TrainingStrings;
|
package/dist/types/d-icons.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type TemplateResult } from 'lit';
|
|
|
3
3
|
* The stroke icons of design D, drawn on the canvas as inline SVG (not Font Awesome). Decorative: hidden from screen
|
|
4
4
|
* readers; the button around them carries the name.
|
|
5
5
|
*/
|
|
6
|
-
export type DIconName = 'speaker' | 'speaker-quiet' | 'check' | 'cross' | 'arrow';
|
|
6
|
+
export type DIconName = 'speaker' | 'speaker-quiet' | 'check' | 'cross' | 'arrow' | 'zoom';
|
|
7
7
|
/** A D icon of the given size in px; strokes in currentColor. */
|
|
8
8
|
export declare function dIcon(name: DIconName, size: number, strokeWidth?: number): TemplateResult;
|
|
9
9
|
/** D's five-point star (24×24 box). */
|
|
@@ -6,7 +6,8 @@ export type CardState = 'question' | 'right' | 'wrong';
|
|
|
6
6
|
* `<octolearn-card>`: design D's question card. Lined paper, a coloured band on top (orange while asking, green after a
|
|
7
7
|
* right answer, red after a wrong one), a header with the octolearn logo, the format label ("DIKTAT"), an optional chip
|
|
8
8
|
* ("Deutsch → Englisch") and the progress stars, and Inkie peeking over the top edge while a question is open. The
|
|
9
|
-
* question goes into the default slot.
|
|
9
|
+
* question goes into the default slot. While the visible area is short (`short`: a phone's keyboard is open) the header
|
|
10
|
+
* and Inkie step aside and the card gets tighter.
|
|
10
11
|
*
|
|
11
12
|
* ```html
|
|
12
13
|
* <octolearn-card label="Diktat" current="3" total="5">
|
|
@@ -9,9 +9,11 @@ export declare const SLOW_RATE = 0.7;
|
|
|
9
9
|
*
|
|
10
10
|
* The element reads the word aloud itself when the question arrives and on the buttons, using the browser's speech
|
|
11
11
|
* synthesis. A page with its own speech output calls `preventDefault()` on `octolearn-speak` / `octolearn-stop`. If the
|
|
12
|
-
* browser blocks speech until the first tap, a "Abspielen" hint appears.
|
|
12
|
+
* browser blocks speech until the first tap, a "Abspielen" hint appears. The word is read in its language (the content's
|
|
13
|
+
* `sourceLanguage`, else German); if the device has no voice for it, a note says so instead of letting another
|
|
14
|
+
* language's voice mispronounce it.
|
|
13
15
|
*
|
|
14
|
-
* @fires octolearn-speak - cancelable, `{ text, reason: 'auto' | 'repeat' | 'slow', rate? }`
|
|
16
|
+
* @fires octolearn-speak - cancelable, `{ text, reason: 'auto' | 'repeat' | 'slow', lang, rate? }`
|
|
15
17
|
* @fires octolearn-stop - cancelable
|
|
16
18
|
* @fires octolearn-settings - "Einstellungen" pressed (the button shows only with the `settings` attribute)
|
|
17
19
|
* @fires octolearn-answer - `{ answer, dontKnow? }`
|
|
@@ -28,8 +30,15 @@ export declare class OctolearnDictationQuestion extends QuestionElement {
|
|
|
28
30
|
/** Reads the word aloud, as the listen button does. */
|
|
29
31
|
speak(reason?: SpeakEventDetail['reason']): void;
|
|
30
32
|
stop(): void;
|
|
33
|
+
connectedCallback(): void;
|
|
34
|
+
disconnectedCallback(): void;
|
|
31
35
|
protected updated(changed: PropertyValues<this>): void;
|
|
32
36
|
render(): TemplateResult | typeof nothing;
|
|
37
|
+
/** The word's language: the content's, else German (octolearn's dictations are German). */
|
|
38
|
+
private get wordLanguage();
|
|
39
|
+
/** Shown once the browser's voice list is known and has no voice for the word's language. */
|
|
40
|
+
private noVoiceHint;
|
|
41
|
+
private readonly handleVoicesChanged;
|
|
33
42
|
private controls;
|
|
34
43
|
private repeat;
|
|
35
44
|
private slow;
|
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
import { nothing, type TemplateResult } from 'lit';
|
|
1
|
+
import { nothing, type PropertyValues, type TemplateResult } from 'lit';
|
|
2
2
|
import { QuestionElement } from './question-parts';
|
|
3
3
|
/**
|
|
4
4
|
* `<octolearn-picture-question>`: design D's "Bilderrätsel": "Was siehst du auf dem Bild?", the picture in a cream frame
|
|
5
5
|
* left of "Schreib das Wort" and the field (above it on narrow screens), or above the answer tiles. `content.question` is
|
|
6
|
-
* the picture's URL.
|
|
6
|
+
* the picture's URL. Tapping the picture shows it large in a modal dialog (closed by a tap anywhere, the close button or
|
|
7
|
+
* Escape); while it is open, the keys don't reach the answer (no number key picks a choice behind it).
|
|
7
8
|
*
|
|
8
9
|
* @fires octolearn-answer - `{ answer, dontKnow? }`
|
|
9
10
|
*/
|
|
10
11
|
export declare class OctolearnPictureQuestion extends QuestionElement {
|
|
11
12
|
static styles: (import("lit").CSSResult | import("lit").CSSResultGroup[])[];
|
|
13
|
+
/** The large picture is showing. */
|
|
14
|
+
private zoomed;
|
|
15
|
+
private zoomDialog?;
|
|
16
|
+
/** Shows the picture large (same as tapping it). */
|
|
17
|
+
enlargePicture(): void;
|
|
18
|
+
/** Closes the large picture. */
|
|
19
|
+
closePicture(): void;
|
|
20
|
+
protected willUpdate(changed: PropertyValues<this>): void;
|
|
21
|
+
protected updated(changed: PropertyValues<this>): void;
|
|
12
22
|
render(): TemplateResult | typeof nothing;
|
|
23
|
+
private onZoomKeydown;
|
|
24
|
+
private onZoomClosed;
|
|
13
25
|
}
|
|
14
26
|
declare global {
|
|
15
27
|
interface HTMLElementTagNameMap {
|
|
@@ -9,8 +9,6 @@ import { QuestionElement } from './question-parts';
|
|
|
9
9
|
* @fires octolearn-speak - cancelable, `{ text, reason: 'read', lang }`
|
|
10
10
|
*/
|
|
11
11
|
export declare class OctolearnQuestionAnswer extends QuestionElement {
|
|
12
|
-
noAutofocus: boolean;
|
|
13
|
-
protected willUpdate(): void;
|
|
14
12
|
render(): TemplateResult | typeof nothing;
|
|
15
13
|
/** A line above tiles or a Wortfragment, saying what to do. */
|
|
16
14
|
private instruction;
|
|
@@ -8,10 +8,13 @@ import type { FlashcardRevealer } from './flashcard';
|
|
|
8
8
|
/** The look shared by the question elements in design D. */
|
|
9
9
|
export declare const questionStyles: import("lit").CSSResult;
|
|
10
10
|
/**
|
|
11
|
-
* Base of the question elements. A new `content` is a new question: the answer field is emptied
|
|
12
|
-
* `
|
|
11
|
+
* Base of the question elements. A new `content` is a new question: the answer field is emptied and the cursor goes into
|
|
12
|
+
* it (the first tile or button without a field); `no-autofocus` leaves the focus where it is. `speak()` reads text aloud through a cancelable `octolearn-speak` event, so a
|
|
13
13
|
* page with its own voice output can take over; otherwise the built-in SpeechController speaks and, if the browser
|
|
14
14
|
* blocks speech until the first tap, a "Abspielen" hint appears.
|
|
15
|
+
*
|
|
16
|
+
* While the visible area is short (a phone's keyboard is open) and the learner is in the answer, the question scrolls the
|
|
17
|
+
* card around it (or itself) to the top of what is visible, so the word or picture isn't hidden above the field.
|
|
15
18
|
*/
|
|
16
19
|
export declare class QuestionElement extends OctolearnElement {
|
|
17
20
|
static styles: import("lit").CSSResultGroup[];
|
|
@@ -21,8 +24,9 @@ export declare class QuestionElement extends OctolearnElement {
|
|
|
21
24
|
noKeys: boolean;
|
|
22
25
|
/** Fetches the answer of a Karteikarte (octolearn's web app), e.g. `content => session.revealFlashcard(content)`. */
|
|
23
26
|
revealFlashcard?: FlashcardRevealer;
|
|
27
|
+
/** Leave the focus where it is when a new question arrives (e.g. a game that keeps the keyboard). */
|
|
28
|
+
noAutofocus: boolean;
|
|
24
29
|
protected needsGesture: boolean;
|
|
25
|
-
protected autofocusAnswer: boolean;
|
|
26
30
|
protected readonly speech: import("..").SpeechController;
|
|
27
31
|
connectedCallback(): void;
|
|
28
32
|
disconnectedCallback(): void;
|
|
@@ -31,6 +35,10 @@ export declare class QuestionElement extends OctolearnElement {
|
|
|
31
35
|
protected playPending(): void;
|
|
32
36
|
protected playHint(t: TrainingStrings): TemplateResult | typeof nothing;
|
|
33
37
|
protected updated(changed: PropertyValues<this>): void;
|
|
38
|
+
/** After the field took the focus (autofocus waits for the field's own update) and the layout settled. */
|
|
39
|
+
private revealWhenFocused;
|
|
40
|
+
/** The `<octolearn-card>` around the question, across shadow roots. */
|
|
41
|
+
private closestCard;
|
|
34
42
|
/** Puts the focus on the answer: the field, the first tile or the first button (e.g. when an overlay opens). */
|
|
35
43
|
focusAnswer(): void;
|
|
36
44
|
}
|
|
@@ -14,6 +14,10 @@ import { OctolearnElement } from '../base-element';
|
|
|
14
14
|
* With `hint` (a Wortfragment, OC-126: "S____e") the known letters stand big above the field, each missing one a dashed
|
|
15
15
|
* box (design D's Lückenwort look); screen readers hear "Hinweis: 6 Zeichen, beginnt mit „S“, endet mit „e“.".
|
|
16
16
|
*
|
|
17
|
+
* While the visible area is short (`short`: a phone's keyboard covers half the screen) it takes as little height as it
|
|
18
|
+
* can: a round send button at the end of the field instead of "Fertig", the visible label as placeholder, the picture
|
|
19
|
+
* small left of the field and "Weiß ich nicht" as a small text button below.
|
|
20
|
+
*
|
|
17
21
|
* @slot before - only with variant="box": shown left of the box
|
|
18
22
|
* @slot media - only with variant="media": shown left of the field
|
|
19
23
|
* @fires octolearn-answer - `{ answer, dontKnow? }`
|
|
@@ -38,6 +42,9 @@ export declare class OctolearnTextAnswer extends OctolearnElement {
|
|
|
38
42
|
/** Puts the cursor into the field. */
|
|
39
43
|
focus(options?: FocusOptions): void;
|
|
40
44
|
render(): TemplateResult;
|
|
45
|
+
private renderActions;
|
|
46
|
+
/** Short: the send button is in the field; only "Weiß ich nicht", small, below it. */
|
|
47
|
+
private renderShortActions;
|
|
41
48
|
private renderHint;
|
|
42
49
|
private onInput;
|
|
43
50
|
private onSubmit;
|
|
@@ -15,6 +15,8 @@ export interface TrainingStrings {
|
|
|
15
15
|
stop: string;
|
|
16
16
|
playHintMessage: string;
|
|
17
17
|
playHintButton: string;
|
|
18
|
+
/** {language} */
|
|
19
|
+
noVoiceMessage: string;
|
|
18
20
|
feedbackCorrect: string;
|
|
19
21
|
feedbackWrong: string;
|
|
20
22
|
yourAnswer: string;
|
|
@@ -59,6 +61,9 @@ export interface TrainingStrings {
|
|
|
59
61
|
pictureAlt: string;
|
|
60
62
|
pictureCaptionVocabulary: string;
|
|
61
63
|
pictureCaptionRiddle: string;
|
|
64
|
+
/** the button on the picture that shows it large */
|
|
65
|
+
enlargePicture: string;
|
|
66
|
+
closePicture: string;
|
|
62
67
|
enterPictureVocabulary: string;
|
|
63
68
|
enterPictureRiddle: string;
|
|
64
69
|
arithmeticHeading: string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -29,3 +29,4 @@ export { createThirdPartyTransport, type ThirdPartyTransportOptions } from './se
|
|
|
29
29
|
export { TrainingSession, type AnswerOutcome, type PairOutcome, type RoundProgress, type SessionState, type TrainingSessionOptions, } from './session/training-session';
|
|
30
30
|
export { TransportError, type AnswerEvaluation, type TodaysStatistic, type Transport } from './session/transport';
|
|
31
31
|
export { getSpeechController, MAX_RATE, MIN_RATE, SpeechController, type SpeakOptions, type SpeakResult, type SpeechControllerOptions, } from './speech';
|
|
32
|
+
export { getViewportWatcher, SHORT_VIEWPORT_PX, ViewportWatcher } from './viewport';
|
package/dist/types/speech.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** The range of octolearn's speed slider. */
|
|
2
2
|
export declare const MIN_RATE = 0.5;
|
|
3
3
|
export declare const MAX_RATE = 1.5;
|
|
4
|
+
/** How long a word waits for the browser's voice list before it is spoken with the language alone. */
|
|
5
|
+
export declare const VOICE_WAIT_MS = 1000;
|
|
4
6
|
/**
|
|
5
7
|
* - `spoken`: speaking (or queued until the browser has loaded its voices)
|
|
6
8
|
* - `blocked`: the browser wants a user gesture first (autoplay policy); call playPending() from a click
|
|
@@ -10,19 +12,23 @@ export type SpeakResult = 'spoken' | 'blocked' | 'unavailable';
|
|
|
10
12
|
export interface SpeakOptions {
|
|
11
13
|
/** Speed for this request only, e.g. 0.7 for "Langsam anhören"; default: the learner's speed. */
|
|
12
14
|
rate?: number;
|
|
13
|
-
/** Language of the text (e.g. "en" for a vocabulary word);
|
|
15
|
+
/** Language of the text (e.g. "en" for a vocabulary word); default: the controller's `preferredLanguage`. */
|
|
14
16
|
lang?: string;
|
|
15
17
|
}
|
|
16
18
|
export interface SpeechControllerOptions {
|
|
17
19
|
/** Voice names tried first when the learner hasn't chosen one. Default: Google Deutsch. */
|
|
18
20
|
preferredVoices?: readonly string[];
|
|
19
|
-
/**
|
|
21
|
+
/** Language of text spoken without a `lang` (default `de`); also the default voice of a settings dialog. */
|
|
20
22
|
preferredLanguage?: string;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Reads dictation words aloud with the Web Speech API. Ported from octolearn's training page: the voice and speed the
|
|
24
26
|
* learner chose are remembered, the newest request wins, and a browser that blocks speech until the first user gesture
|
|
25
27
|
* keeps the text until playPending() is called from one.
|
|
28
|
+
*
|
|
29
|
+
* A text is only read by a voice of its language: the learner's voice if it speaks that language, else a preferred
|
|
30
|
+
* voice or the first voice of it. Without one the browser gets only the language, never a voice of another language
|
|
31
|
+
* (left to itself, Chrome takes the voice of the browser's language, so German words came out English).
|
|
26
32
|
*/
|
|
27
33
|
export declare class SpeechController {
|
|
28
34
|
voices: SpeechSynthesisVoice[];
|
|
@@ -31,6 +37,7 @@ export declare class SpeechController {
|
|
|
31
37
|
private pending;
|
|
32
38
|
private requestId;
|
|
33
39
|
private connections;
|
|
40
|
+
private voiceWait;
|
|
34
41
|
private readonly preferredVoices;
|
|
35
42
|
private readonly preferredLanguage;
|
|
36
43
|
constructor(options?: SpeechControllerOptions);
|
|
@@ -41,11 +48,21 @@ export declare class SpeechController {
|
|
|
41
48
|
speak(input: string | null | undefined, options?: SpeakOptions): SpeakResult;
|
|
42
49
|
/** Speaks the text that was blocked; call it from a click. */
|
|
43
50
|
playPending(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the browser has a voice for the language (default: `preferredLanguage`); `undefined` while its voice list
|
|
53
|
+
* is still loading or without speech synthesis.
|
|
54
|
+
*/
|
|
55
|
+
hasVoiceFor(lang?: string): boolean | undefined;
|
|
44
56
|
stop(): void;
|
|
45
57
|
/** Remembers the voice (by name; the voice objects change when the browser reloads its list) and the speed. */
|
|
46
58
|
choose(voice: SpeechSynthesisVoice | null, rate?: number): void;
|
|
47
59
|
private readonly handleVoicesChanged;
|
|
60
|
+
/** Speaks the pending text now if the voice list is known, else on voiceschanged or after VOICE_WAIT_MS. */
|
|
61
|
+
private flushWhenVoicesKnown;
|
|
62
|
+
private stopWaitingForVoices;
|
|
63
|
+
/** Reads the browser's voice list; false while it is still empty. */
|
|
48
64
|
private refreshVoices;
|
|
65
|
+
/** A voice that speaks the language: the learner's, else a preferred one, else the first; never one of another language. */
|
|
49
66
|
private voiceFor;
|
|
50
67
|
private preferredVoice;
|
|
51
68
|
private flushPending;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - `.prompt` the handwritten instruction, `.label` the uppercase format label
|
|
8
8
|
* - `.keys-only` for hints about keys ("Enter drücken oder"), hidden on touch screens
|
|
9
9
|
* Sizes are D's at 1280 px. Below 560 px of width (the element's `.root`, or the card around it) they shrink to
|
|
10
|
-
* phone sizes; elements that use these parts set `container-type: inline-size` on their `.root`.
|
|
10
|
+
* phone sizes; elements that use these parts set `container-type: inline-size` on their `.root`. While the visible area
|
|
11
|
+
* is short (`:host([short])`, a phone's keyboard is open) they shrink further.
|
|
11
12
|
*/
|
|
12
13
|
export declare const dParts: import("lit").CSSResult;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the visible part of the page short, e.g. because a phone's on-screen keyboard covers half of it? CSS can't tell:
|
|
3
|
+
* iOS Safari and current Android Chrome shrink only the visual viewport when the keyboard opens, not the layout viewport
|
|
4
|
+
* that `vh` and media queries see. So the elements ask `window.visualViewport`; while it is short they switch to a
|
|
5
|
+
* compact layout (attribute `short`, see OctolearnElement).
|
|
6
|
+
*/
|
|
7
|
+
/** Below this height (CSS px, at zoom 1) the visible area counts as short. A phone with its keyboard open has ~300–400. */
|
|
8
|
+
export declare const SHORT_VIEWPORT_PX = 520;
|
|
9
|
+
type Listener = (short: boolean) => void;
|
|
10
|
+
/** Watches `window.visualViewport` while anything listens. One per page: see getViewportWatcher(). */
|
|
11
|
+
export declare class ViewportWatcher {
|
|
12
|
+
private readonly listeners;
|
|
13
|
+
private viewport;
|
|
14
|
+
private current;
|
|
15
|
+
/** The visible area is short right now (false without a visual viewport, e.g. old browsers). */
|
|
16
|
+
get short(): boolean;
|
|
17
|
+
/** Calls `listener` whenever `short` flips; returns the function that ends it. */
|
|
18
|
+
subscribe(listener: Listener): () => void;
|
|
19
|
+
private attach;
|
|
20
|
+
private detach;
|
|
21
|
+
private readonly onResize;
|
|
22
|
+
private static visualViewport;
|
|
23
|
+
/** Height times scale: zooming in with two fingers makes the visual viewport smaller, but the page isn't short. */
|
|
24
|
+
private static measure;
|
|
25
|
+
}
|
|
26
|
+
/** The watcher the elements use. One per page: there is one viewport. */
|
|
27
|
+
export declare function getViewportWatcher(): ViewportWatcher;
|
|
28
|
+
/**
|
|
29
|
+
* Scrolls `element` to the top of what the learner sees. On iOS the visual viewport is panned down inside the layout
|
|
30
|
+
* viewport while the keyboard is open (`offsetTop`); without the correction the element would sit above the visible part.
|
|
31
|
+
*/
|
|
32
|
+
export declare function revealAtTop(element: Element): void;
|
|
33
|
+
/** The focus is inside `element` (also inside its shadow root and the shadow roots below). */
|
|
34
|
+
export declare function hasFocusInside(element: Element): boolean;
|
|
35
|
+
export {};
|
package/package.json
CHANGED