@mpgd/tutorial 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.
@@ -0,0 +1,19 @@
1
+ export * from './definition.js';
2
+ export * from './director.js';
3
+ export * from './progress.js';
4
+ export declare const tutorialDomAttributes: Readonly<{
5
+ active: "data-mpgd-tutorial-active";
6
+ next: "data-mpgd-tutorial-next";
7
+ popover: "data-mpgd-tutorial-popover";
8
+ skip: "data-mpgd-tutorial-skip";
9
+ step: "data-mpgd-tutorial-step";
10
+ target: "data-mpgd-tutorial-target";
11
+ }>;
12
+ export declare const tutorialDomSelectors: Readonly<{
13
+ active: "[data-mpgd-tutorial-active=\"true\"]";
14
+ next: "[data-mpgd-tutorial-next]";
15
+ popover: "[data-mpgd-tutorial-popover]";
16
+ skip: "[data-mpgd-tutorial-skip]";
17
+ step: "[data-mpgd-tutorial-step]";
18
+ target: "[data-mpgd-tutorial-target]";
19
+ }>;
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ export * from './definition.js';
2
+ export * from './director.js';
3
+ export * from './progress.js';
4
+ export const tutorialDomAttributes = Object.freeze({
5
+ active: 'data-mpgd-tutorial-active',
6
+ next: 'data-mpgd-tutorial-next',
7
+ popover: 'data-mpgd-tutorial-popover',
8
+ skip: 'data-mpgd-tutorial-skip',
9
+ step: 'data-mpgd-tutorial-step',
10
+ target: 'data-mpgd-tutorial-target',
11
+ });
12
+ export const tutorialDomSelectors = Object.freeze({
13
+ active: '[data-mpgd-tutorial-active="true"]',
14
+ next: '[data-mpgd-tutorial-next]',
15
+ popover: '[data-mpgd-tutorial-popover]',
16
+ skip: '[data-mpgd-tutorial-skip]',
17
+ step: '[data-mpgd-tutorial-step]',
18
+ target: '[data-mpgd-tutorial-target]',
19
+ });
@@ -0,0 +1,14 @@
1
+ import type { StorageAdapter } from '@mpgd/platform';
2
+ import type { TutorialDefinition } from './definition.js';
3
+ import { type TutorialProgressOf, type TutorialProgressStore } from './progress.js';
4
+ export declare const defaultTutorialStorageLoadTimeoutMs = 10000;
5
+ export interface CreatePlatformTutorialProgressStoreInput<TDefinition extends TutorialDefinition> {
6
+ readonly definition: TDefinition;
7
+ readonly invalidRecord?: 'disable' | 'ignore';
8
+ readonly key: string;
9
+ readonly migrate?: (value: unknown) => unknown;
10
+ readonly onError?: (error: unknown) => void;
11
+ readonly storage: Pick<StorageAdapter, 'load' | 'save'>;
12
+ readonly loadTimeoutMs?: number;
13
+ }
14
+ export declare function createPlatformTutorialProgressStore<TDefinition extends TutorialDefinition>(input: CreatePlatformTutorialProgressStoreInput<TDefinition>): Promise<TutorialProgressStore<TutorialProgressOf<TDefinition>>>;
@@ -0,0 +1,74 @@
1
+ import { parseTutorialProgress, } from './progress.js';
2
+ export const defaultTutorialStorageLoadTimeoutMs = 10000;
3
+ export async function createPlatformTutorialProgressStore(input) {
4
+ const loadTimeoutMs = resolveLoadTimeoutMs(input.loadTimeoutMs);
5
+ let current = null;
6
+ let available = true;
7
+ let queue = Promise.resolve();
8
+ try {
9
+ const stored = await loadWithTimeout(input.storage.load({ key: input.key }), loadTimeoutMs);
10
+ if (stored !== null) {
11
+ const migrated = input.migrate === undefined ? stored.value : input.migrate(stored.value);
12
+ current = parseTutorialProgress(input.definition, migrated);
13
+ if (current === null && (input.invalidRecord ?? 'disable') === 'disable') {
14
+ available = false;
15
+ input.onError?.(new Error(`Invalid tutorial progress: ${input.key}`));
16
+ }
17
+ }
18
+ }
19
+ catch (error) {
20
+ available = false;
21
+ input.onError?.(error);
22
+ }
23
+ return {
24
+ get available() {
25
+ return available;
26
+ },
27
+ async flush() {
28
+ await queue;
29
+ },
30
+ getSnapshot: () => current,
31
+ save(progress) {
32
+ if (!available) {
33
+ return Promise.resolve();
34
+ }
35
+ const operation = queue.then(async () => {
36
+ if (!available) {
37
+ return;
38
+ }
39
+ await input.storage.save({ key: input.key, value: progress });
40
+ current = progress;
41
+ });
42
+ void (queue = operation.catch((error) => {
43
+ available = false;
44
+ input.onError?.(error);
45
+ }));
46
+ return operation;
47
+ },
48
+ };
49
+ }
50
+ function resolveLoadTimeoutMs(timeoutMs) {
51
+ const resolved = timeoutMs ?? defaultTutorialStorageLoadTimeoutMs;
52
+ if (!Number.isFinite(resolved) || resolved <= 0) {
53
+ throw new TypeError('Tutorial storage loadTimeoutMs must be a positive finite number.');
54
+ }
55
+ return resolved;
56
+ }
57
+ async function loadWithTimeout(operation, timeoutMs) {
58
+ let timer;
59
+ try {
60
+ return await Promise.race([
61
+ operation,
62
+ new Promise((_resolve, reject) => {
63
+ timer = globalThis.setTimeout(() => {
64
+ reject(new Error(`Tutorial storage load timed out after ${timeoutMs}ms.`));
65
+ }, timeoutMs);
66
+ }),
67
+ ]);
68
+ }
69
+ finally {
70
+ if (timer !== undefined) {
71
+ globalThis.clearTimeout(timer);
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,23 @@
1
+ import type { TutorialDefinition, TutorialSceneOf, TutorialStepIdOf } from './definition.js';
2
+ export type TutorialProgressStatus = 'active' | 'completed' | 'skipped';
3
+ export interface TutorialProgress<TStepId extends string = string, TScene extends string = string> {
4
+ readonly checkpoint: TScene;
5
+ readonly completedAt: string | null;
6
+ readonly completedStepIds: readonly TStepId[];
7
+ readonly definitionRevision: number;
8
+ readonly schemaVersion: 1;
9
+ readonly skippedAt: string | null;
10
+ readonly status: TutorialProgressStatus;
11
+ readonly tutorialId: string;
12
+ readonly updatedAt: string;
13
+ }
14
+ export type TutorialProgressOf<TDefinition extends TutorialDefinition> = TutorialProgress<TutorialStepIdOf<TDefinition>, TutorialSceneOf<TDefinition>>;
15
+ export interface TutorialProgressStore<TProgress extends TutorialProgress = TutorialProgress> {
16
+ readonly available: boolean;
17
+ flush(): Promise<void>;
18
+ getSnapshot(): TProgress | null;
19
+ save(progress: TProgress): Promise<void>;
20
+ }
21
+ export declare function createInitialTutorialProgress<TDefinition extends TutorialDefinition>(definition: TDefinition, now?: string): TutorialProgressOf<TDefinition>;
22
+ export declare function createTutorialProgressAtStep<TDefinition extends TutorialDefinition>(definition: TDefinition, stepId: TutorialStepIdOf<TDefinition>, now?: string): TutorialProgressOf<TDefinition>;
23
+ export declare function parseTutorialProgress<TDefinition extends TutorialDefinition>(definition: TDefinition, value: unknown): TutorialProgressOf<TDefinition> | null;
@@ -0,0 +1,80 @@
1
+ export function createInitialTutorialProgress(definition, now = new Date().toISOString()) {
2
+ return {
3
+ checkpoint: definition.initialScene,
4
+ completedAt: null,
5
+ completedStepIds: [],
6
+ definitionRevision: definition.revision,
7
+ schemaVersion: 1,
8
+ skippedAt: null,
9
+ status: 'active',
10
+ tutorialId: definition.id,
11
+ updatedAt: now,
12
+ };
13
+ }
14
+ export function createTutorialProgressAtStep(definition, stepId, now = new Date().toISOString()) {
15
+ const stepIndex = definition.steps.findIndex((step) => step.id === stepId);
16
+ if (stepIndex < 0) {
17
+ throw new Error(`Unknown tutorial step: ${stepId}`);
18
+ }
19
+ const step = definition.steps[stepIndex];
20
+ if (step === undefined) {
21
+ throw new Error(`Unknown tutorial step: ${stepId}`);
22
+ }
23
+ return {
24
+ ...createInitialTutorialProgress(definition, now),
25
+ checkpoint: step.scene,
26
+ completedStepIds: definition.steps.slice(0, stepIndex).map((candidate) => candidate.id),
27
+ };
28
+ }
29
+ export function parseTutorialProgress(definition, value) {
30
+ if (!isRecord(value)
31
+ || value.schemaVersion !== 1
32
+ || value.tutorialId !== definition.id
33
+ || value.definitionRevision !== definition.revision
34
+ || !isProgressStatus(value.status)
35
+ || !isTimestamp(value.updatedAt)
36
+ || !(value.completedAt === null || isTimestamp(value.completedAt))
37
+ || !(value.skippedAt === null || isTimestamp(value.skippedAt))
38
+ || typeof value.checkpoint !== 'string'
39
+ || !Array.isArray(value.completedStepIds)) {
40
+ return null;
41
+ }
42
+ const scenes = new Set(definition.steps.map((step) => step.scene));
43
+ const completedStepIds = value.completedStepIds;
44
+ if (!scenes.has(value.checkpoint)
45
+ || completedStepIds.some((id) => typeof id !== 'string')
46
+ || new Set(completedStepIds).size !== completedStepIds.length
47
+ || completedStepIds.length > definition.steps.length
48
+ || completedStepIds.some((id, index) => definition.steps[index]?.id !== id)) {
49
+ return null;
50
+ }
51
+ const complete = completedStepIds.length === definition.steps.length;
52
+ if ((value.status === 'active'
53
+ && (complete || value.completedAt !== null || value.skippedAt !== null))
54
+ || (value.status === 'completed'
55
+ && (!complete || value.completedAt === null || value.skippedAt !== null))
56
+ || (value.status === 'skipped'
57
+ && (complete || value.completedAt !== null || value.skippedAt === null))) {
58
+ return null;
59
+ }
60
+ return {
61
+ checkpoint: value.checkpoint,
62
+ completedAt: value.completedAt,
63
+ completedStepIds: [...completedStepIds],
64
+ definitionRevision: definition.revision,
65
+ schemaVersion: 1,
66
+ skippedAt: value.skippedAt,
67
+ status: value.status,
68
+ tutorialId: definition.id,
69
+ updatedAt: value.updatedAt,
70
+ };
71
+ }
72
+ function isProgressStatus(value) {
73
+ return value === 'active' || value === 'completed' || value === 'skipped';
74
+ }
75
+ function isTimestamp(value) {
76
+ return typeof value === 'string' && Number.isFinite(Date.parse(value));
77
+ }
78
+ function isRecord(value) {
79
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
80
+ }
@@ -0,0 +1,59 @@
1
+ import type { TutorialActionOf, TutorialDefinition, TutorialSignalOf, TutorialStepIdOf } from './definition.js';
2
+ import type { TutorialDirector, TutorialReplayOptions } from './director.js';
3
+ import type { TutorialProgress, TutorialProgressOf, TutorialProgressStore } from './progress.js';
4
+ export interface MemoryTutorialProgressStore<TProgress extends TutorialProgress> extends TutorialProgressStore<TProgress> {
5
+ readonly saves: readonly TProgress[];
6
+ }
7
+ export declare function createMemoryTutorialProgressStore(): MemoryTutorialProgressStore<TutorialProgress>;
8
+ export declare function createMemoryTutorialProgressStore<TDefinition extends TutorialDefinition>(input: {
9
+ readonly definition: TDefinition;
10
+ readonly available?: boolean;
11
+ readonly initial?: TutorialProgressOf<TDefinition> | null;
12
+ readonly rejectSave?: boolean;
13
+ }): MemoryTutorialProgressStore<TutorialProgressOf<TDefinition>>;
14
+ export declare function createMemoryTutorialProgressStore<TProgress extends TutorialProgress>(input?: {
15
+ readonly available?: boolean;
16
+ readonly definition?: TutorialDefinition;
17
+ readonly initial?: TProgress | null;
18
+ readonly rejectSave?: boolean;
19
+ }): MemoryTutorialProgressStore<TProgress>;
20
+ export type TutorialDebugMode = 'default' | 'off' | 'replay';
21
+ export interface TutorialDebugLaunchPolicy<TStepId extends string = string> {
22
+ readonly enabled: boolean;
23
+ readonly mode: TutorialDebugMode;
24
+ readonly stepId: TStepId | null;
25
+ }
26
+ export declare function resolveTutorialDebugLaunchPolicy<TDefinition extends TutorialDefinition>(input: {
27
+ readonly definition: TDefinition;
28
+ readonly enabled: boolean;
29
+ readonly search: string;
30
+ }): TutorialDebugLaunchPolicy<TutorialStepIdOf<TDefinition>>;
31
+ export declare function applyTutorialDebugLaunchPolicy<TDefinition extends TutorialDefinition>(director: TutorialDirector<TDefinition>, policy: TutorialDebugLaunchPolicy<TutorialStepIdOf<TDefinition>>): Promise<void>;
32
+ export interface TutorialDebugBridge<TDefinition extends TutorialDefinition> {
33
+ action(action: TutorialActionOf<TDefinition>): void;
34
+ getSnapshot(): ReturnType<TutorialDirector<TDefinition>['getSnapshot']>;
35
+ goToStep(stepId: TutorialStepIdOf<TDefinition>): Promise<void>;
36
+ replay(): Promise<void>;
37
+ scene(scene: string): void;
38
+ signal(signal: TutorialSignalOf<TDefinition>): void;
39
+ skip(): Promise<void>;
40
+ suspend(suspended: boolean): void;
41
+ }
42
+ export interface InstallTutorialDebugBridgeInput<TDefinition extends TutorialDefinition> {
43
+ readonly afterReplay?: (options: TutorialReplayOptions<TDefinition>) => Promise<void> | void;
44
+ readonly beforeReplay?: (options: TutorialReplayOptions<TDefinition>) => Promise<void> | void;
45
+ readonly director: TutorialDirector<TDefinition>;
46
+ readonly floatingReplayTrigger?: false | {
47
+ readonly ariaLabel?: string;
48
+ readonly className?: string;
49
+ readonly label?: string;
50
+ readonly parent?: HTMLElement;
51
+ };
52
+ readonly globalKey?: string;
53
+ readonly globalObject?: typeof globalThis;
54
+ readonly onError?: (error: unknown) => void;
55
+ }
56
+ export declare function installTutorialDebugBridge<TDefinition extends TutorialDefinition>(input: InstallTutorialDebugBridgeInput<TDefinition>): {
57
+ readonly bridge: TutorialDebugBridge<TDefinition>;
58
+ destroy(): void;
59
+ };
@@ -0,0 +1,156 @@
1
+ export function createMemoryTutorialProgressStore(input = {}) {
2
+ const available = input.available ?? true;
3
+ let current = input.initial ?? null;
4
+ const saves = [];
5
+ return {
6
+ available,
7
+ flush: () => Promise.resolve(),
8
+ get saves() {
9
+ return saves;
10
+ },
11
+ getSnapshot: () => current,
12
+ async save(progress) {
13
+ if (!available) {
14
+ return;
15
+ }
16
+ if (input.rejectSave === true) {
17
+ throw new Error('Memory tutorial progress store rejected a save.');
18
+ }
19
+ current = progress;
20
+ saves.push(progress);
21
+ },
22
+ };
23
+ }
24
+ export function resolveTutorialDebugLaunchPolicy(input) {
25
+ if (!input.enabled) {
26
+ return { enabled: false, mode: 'default', stepId: null };
27
+ }
28
+ const parameters = new URLSearchParams(input.search);
29
+ const requestedMode = parameters.get('mpgd-tutorial');
30
+ const requestedStepId = parameters.get('mpgd-tutorial-step');
31
+ const mode = requestedMode === 'off'
32
+ ? 'off'
33
+ : requestedMode === 'replay' || requestedStepId !== null
34
+ ? 'replay'
35
+ : 'default';
36
+ return {
37
+ enabled: true,
38
+ mode,
39
+ stepId: requestedStepId !== null && isTutorialStepId(input.definition, requestedStepId)
40
+ ? requestedStepId
41
+ : null,
42
+ };
43
+ }
44
+ export async function applyTutorialDebugLaunchPolicy(director, policy) {
45
+ if (!policy.enabled || policy.mode === 'default' && policy.stepId === null) {
46
+ return;
47
+ }
48
+ if (policy.mode === 'off') {
49
+ director.setSuspended(true);
50
+ return;
51
+ }
52
+ const options = policy.stepId === null
53
+ ? {}
54
+ : { fromStepId: policy.stepId };
55
+ await director.replay(options);
56
+ }
57
+ function isTutorialStepId(definition, stepId) {
58
+ return definition.steps.some((step) => step.id === stepId);
59
+ }
60
+ const tutorialDebugBridgeInstallations = new WeakMap();
61
+ export function installTutorialDebugBridge(input) {
62
+ const globalObject = input.globalObject ?? globalThis;
63
+ const globalKey = input.globalKey ?? '__MPGD_TUTORIAL__';
64
+ const record = globalObject;
65
+ const previousHadOwnProperty = Object.hasOwn(record, globalKey);
66
+ const previous = record[globalKey];
67
+ const replay = async (options = {}) => {
68
+ await input.beforeReplay?.(options);
69
+ await input.director.replay(options);
70
+ await input.afterReplay?.(options);
71
+ };
72
+ const bridge = {
73
+ action: (action) => input.director.observeAction(action),
74
+ getSnapshot: () => input.director.getSnapshot(),
75
+ goToStep: (stepId) => replay({ fromStepId: stepId }),
76
+ replay: () => replay(),
77
+ scene: (scene) => input.director.observeScene(scene),
78
+ signal: (signal) => input.director.observeSignal(signal),
79
+ skip: () => input.director.skip(),
80
+ suspend: (suspended) => input.director.setSuspended(suspended),
81
+ };
82
+ const possiblePredecessor = typeof previous === 'object' && previous !== null
83
+ ? tutorialDebugBridgeInstallations.get(previous)
84
+ : undefined;
85
+ const predecessor = possiblePredecessor?.record === record
86
+ && possiblePredecessor.globalKey === globalKey
87
+ ? possiblePredecessor
88
+ : null;
89
+ const installation = {
90
+ bridge,
91
+ destroyed: false,
92
+ fallback: predecessor === null ? previous : predecessor.fallback,
93
+ fallbackHadOwnProperty: predecessor === null
94
+ ? previousHadOwnProperty
95
+ : predecessor.fallbackHadOwnProperty,
96
+ globalKey,
97
+ predecessor,
98
+ record,
99
+ };
100
+ tutorialDebugBridgeInstallations.set(bridge, installation);
101
+ record[globalKey] = bridge;
102
+ const destroyTrigger = createFloatingReplayTrigger(input, bridge);
103
+ return {
104
+ bridge,
105
+ destroy() {
106
+ if (installation.destroyed) {
107
+ return;
108
+ }
109
+ installation.destroyed = true;
110
+ destroyTrigger?.();
111
+ if (record[globalKey] !== bridge) {
112
+ return;
113
+ }
114
+ let restoredInstallation = installation.predecessor;
115
+ while (restoredInstallation?.destroyed === true) {
116
+ restoredInstallation = restoredInstallation.predecessor;
117
+ }
118
+ if (restoredInstallation !== null) {
119
+ record[globalKey] = restoredInstallation.bridge;
120
+ }
121
+ else if (installation.fallbackHadOwnProperty) {
122
+ record[globalKey] = installation.fallback;
123
+ }
124
+ else {
125
+ Reflect.deleteProperty(record, globalKey);
126
+ }
127
+ },
128
+ };
129
+ }
130
+ function createFloatingReplayTrigger(input, bridge) {
131
+ if (input.floatingReplayTrigger === false
132
+ || input.floatingReplayTrigger === undefined
133
+ || typeof document === 'undefined') {
134
+ return null;
135
+ }
136
+ const options = input.floatingReplayTrigger;
137
+ const button = document.createElement('button');
138
+ button.type = 'button';
139
+ button.textContent = options.label ?? '?';
140
+ button.setAttribute('aria-label', options.ariaLabel ?? 'Replay tutorial');
141
+ button.dataset.mpgdTutorialDebugReplay = 'true';
142
+ button.className = options.className ?? '';
143
+ button.style.position = 'fixed';
144
+ button.style.insetBlockStart = '12px';
145
+ button.style.insetInlineEnd = '12px';
146
+ button.style.zIndex = '2147483647';
147
+ const handleClick = () => {
148
+ void bridge.replay().catch((error) => input.onError?.(error));
149
+ };
150
+ button.addEventListener('click', handleClick);
151
+ (options.parent ?? document.body).appendChild(button);
152
+ return () => {
153
+ button.removeEventListener('click', handleClick);
154
+ button.remove();
155
+ };
156
+ }
package/driver.css ADDED
@@ -0,0 +1,91 @@
1
+ @import 'driver.js/dist/driver.css';
2
+
3
+ .mpgd-tutorial-popover.driver-popover {
4
+ --mpgd-tutorial-accent: #ff8768;
5
+ --mpgd-tutorial-focus: #76cbd1;
6
+ --mpgd-tutorial-foreground: #fff0dc;
7
+ --mpgd-tutorial-muted: #aebbc0;
8
+ --mpgd-tutorial-on-accent: #24110d;
9
+ --mpgd-tutorial-surface: rgb(29 35 39 / 98%);
10
+
11
+ box-sizing: border-box;
12
+ min-width: min(280px, calc(100vw - 28px));
13
+ max-width: min(360px, calc(100vw - 28px));
14
+ padding: 16px;
15
+ border: 1px solid rgb(239 226 205 / 22%);
16
+ border-top: 3px solid var(--mpgd-tutorial-accent);
17
+ border-radius: 12px;
18
+ background: var(--mpgd-tutorial-surface);
19
+ box-shadow: 0 24px 70px rgb(0 0 0 / 52%);
20
+ color: var(--mpgd-tutorial-foreground);
21
+ font-family: system-ui, sans-serif;
22
+ }
23
+
24
+ .mpgd-tutorial-popover .driver-popover-title {
25
+ padding-inline-end: 45%;
26
+ color: var(--mpgd-tutorial-foreground);
27
+ font-size: 18px;
28
+ font-weight: 800;
29
+ line-height: 1.25;
30
+ }
31
+
32
+ .mpgd-tutorial-popover .driver-popover-description {
33
+ margin-top: 7px;
34
+ color: var(--mpgd-tutorial-muted);
35
+ font-size: 13px;
36
+ line-height: 1.55;
37
+ }
38
+
39
+ .mpgd-tutorial-popover .driver-popover-close-btn {
40
+ width: auto;
41
+ min-width: 36px;
42
+ height: 36px;
43
+ padding-inline: 8px;
44
+ max-width: calc(45% - 8px);
45
+ overflow: hidden;
46
+ color: var(--mpgd-tutorial-muted);
47
+ font-size: 11px;
48
+ font-weight: 700;
49
+ text-overflow: ellipsis;
50
+ white-space: nowrap;
51
+ }
52
+
53
+ .mpgd-tutorial-popover .driver-popover-footer-btn {
54
+ min-width: 86px;
55
+ min-height: 42px;
56
+ padding: 0 14px;
57
+ border: 1px solid var(--mpgd-tutorial-accent);
58
+ border-radius: 8px;
59
+ background: var(--mpgd-tutorial-accent);
60
+ color: var(--mpgd-tutorial-on-accent);
61
+ font: inherit;
62
+ font-size: 13px;
63
+ font-weight: 800;
64
+ }
65
+
66
+ .mpgd-tutorial-popover :is(button, [href]):focus-visible {
67
+ outline: 2px solid var(--mpgd-tutorial-focus);
68
+ outline-offset: 2px;
69
+ }
70
+
71
+ [data-mpgd-tutorial-active='true'] .driver-active-element {
72
+ outline: 2px solid var(--mpgd-tutorial-focus, #76cbd1);
73
+ outline-offset: 2px;
74
+ }
75
+
76
+ @supports (color: color-mix(in srgb, red, blue)) {
77
+ [data-mpgd-tutorial-active='true'] .driver-active-element {
78
+ outline: 2px solid color-mix(
79
+ in srgb,
80
+ var(--mpgd-tutorial-focus, #76cbd1) 52%,
81
+ transparent
82
+ );
83
+ }
84
+ }
85
+
86
+ @media (prefers-reduced-motion: reduce) {
87
+ .mpgd-tutorial-popover.driver-popover,
88
+ .driver-overlay {
89
+ animation: none;
90
+ }
91
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@mpgd/tutorial",
3
+ "version": "0.1.0",
4
+ "description": "Headless tutorial progression, optional Driver.js presentation, and local reproduction helpers for mpgd games.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/imjlk/mpgd-kit.git",
9
+ "directory": "packages/tutorial"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/imjlk/mpgd-kit/issues"
13
+ },
14
+ "homepage": "https://github.com/imjlk/mpgd-kit#readme",
15
+ "keywords": [
16
+ "mpgd",
17
+ "game-development",
18
+ "tutorial",
19
+ "onboarding",
20
+ "driver.js"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "type": "module",
26
+ "sideEffects": [
27
+ "./driver.css"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./platform-storage": {
35
+ "types": "./dist/platform-storage.d.ts",
36
+ "default": "./dist/platform-storage.js"
37
+ },
38
+ "./driver": {
39
+ "types": "./dist/driver.d.ts",
40
+ "default": "./dist/driver.js"
41
+ },
42
+ "./testing": {
43
+ "types": "./dist/testing.d.ts",
44
+ "default": "./dist/testing.js"
45
+ },
46
+ "./driver.css": "./driver.css"
47
+ },
48
+ "files": [
49
+ "dist",
50
+ "driver.css"
51
+ ],
52
+ "dependencies": {
53
+ "driver.js": "1.8.0",
54
+ "@mpgd/platform": "0.9.0"
55
+ },
56
+ "devDependencies": {
57
+ "jsdom": "24.1.0",
58
+ "ttsc": "0.18.4",
59
+ "typescript": "7.0.2",
60
+ "vitest": "^4.0.0"
61
+ },
62
+ "main": "./dist/index.js",
63
+ "types": "./dist/index.d.ts",
64
+ "scripts": {
65
+ "check": "ttsc --noEmit",
66
+ "lint": "ttsc --noEmit",
67
+ "test": "vitest run",
68
+ "format": "ttsc format",
69
+ "fix": "ttsc fix"
70
+ }
71
+ }