@flare-engine/a11y 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 +29 -0
- package/dist/index.d.mts +90 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +112 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +81 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Grzegorz Otto
|
|
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
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @flare-engine/a11y
|
|
2
|
+
|
|
3
|
+
Central accessibility state for `@flare-engine/animation`, `@flare-engine/postfx`, and `@flare-engine/ui` to dampen or neutralize motion / contrast / scale per user preference. Light surface — full screen-reader pass deferred to 0.3.0. Hosts bridge OS prefs (`AccessibilityInfo.isReduceMotionEnabled`, `prefers-reduced-motion`) by calling `update()` on startup.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @flare-engine/a11y
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { A11yManager, motionScale, dampenedMotion, pickContrast } from '@flare-engine/a11y';
|
|
15
|
+
|
|
16
|
+
const a11y = new A11yManager();
|
|
17
|
+
a11y.update({ reducedMotion: true, contrast: 'high' });
|
|
18
|
+
a11y.onChange.add((settings) => console.log(settings));
|
|
19
|
+
|
|
20
|
+
const shakeIntensity = motionScale(a11y.settings, 8); // 0 under reduced motion
|
|
21
|
+
const vignettePulse = dampenedMotion(a11y.settings, 0.4); // softens but preserves
|
|
22
|
+
const fg = pickContrast(a11y.settings, 0xff_cc_cc_cc, 0xff_ff_ff_ff);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
See [API spec](../../docs/design/api-specs.md#flare-enginea11y--2kb-raw-light) and [skill reference](../../.github/skills/flare-a11y/SKILL.md).
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Signal } from '@flare-engine/events';
|
|
2
|
+
|
|
3
|
+
type ContrastLevel = 'normal' | 'high';
|
|
4
|
+
interface A11ySettings {
|
|
5
|
+
/** When true, animations and heavy postfx should be minimized. */
|
|
6
|
+
reducedMotion: boolean;
|
|
7
|
+
/** Current contrast preference — UI tokens swap accordingly. */
|
|
8
|
+
contrast: ContrastLevel;
|
|
9
|
+
/** Global UI scale multiplier (`1` = default). */
|
|
10
|
+
uiScale: number;
|
|
11
|
+
}
|
|
12
|
+
interface A11yManagerOptions {
|
|
13
|
+
initial?: Partial<A11ySettings>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Central accessibility state. Systems subscribe via `onChange` and read the
|
|
17
|
+
* current snapshot via `settings`. The manager owns no platform wiring — a
|
|
18
|
+
* host app may bridge OS preferences (e.g., `AccessibilityInfo.isReduceMotionEnabled`)
|
|
19
|
+
* by pushing values in on startup.
|
|
20
|
+
*/
|
|
21
|
+
declare class A11yManager {
|
|
22
|
+
readonly onChange: Signal<A11ySettings>;
|
|
23
|
+
private _settings;
|
|
24
|
+
constructor(options?: A11yManagerOptions);
|
|
25
|
+
get settings(): A11ySettings;
|
|
26
|
+
get reducedMotion(): boolean;
|
|
27
|
+
get contrast(): ContrastLevel;
|
|
28
|
+
get uiScale(): number;
|
|
29
|
+
update(patch: Partial<A11ySettings>): void;
|
|
30
|
+
setReducedMotion(v: boolean): void;
|
|
31
|
+
setContrast(v: ContrastLevel): void;
|
|
32
|
+
setUiScale(v: number): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Strength multiplier used by animation + postfx systems. Caller chooses a
|
|
37
|
+
* base intensity; this function clamps it for reduced-motion users.
|
|
38
|
+
*/
|
|
39
|
+
declare function motionScale(settings: {
|
|
40
|
+
reducedMotion: boolean;
|
|
41
|
+
}, base?: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* Returns a second, dampened intensity. Useful for effects you want to keep
|
|
44
|
+
* but soften (camera shake, chromatic aberration). `reducedFactor` controls
|
|
45
|
+
* how much survives when reduced-motion is on (default 20%).
|
|
46
|
+
*/
|
|
47
|
+
declare function dampenedMotion(settings: {
|
|
48
|
+
reducedMotion: boolean;
|
|
49
|
+
}, base: number, reducedFactor?: number): number;
|
|
50
|
+
/**
|
|
51
|
+
* Helper for UI layers: pick a color based on contrast preference.
|
|
52
|
+
*/
|
|
53
|
+
declare function pickContrast<T>(settings: Pick<A11ySettings, 'contrast'>, normal: T, high: T): T;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A11yBridge — one-liner wiring of an `A11yManager` into runtime systems that
|
|
57
|
+
* expose `setMotionScale(k)`. Typical targets: `Camera2D` (screen-shake) and
|
|
58
|
+
* `PostFxController` (global effect intensity).
|
|
59
|
+
*
|
|
60
|
+
* The bridge owns no platform wiring — callers push OS preferences (e.g.
|
|
61
|
+
* `AccessibilityInfo.isReduceMotionEnabled()`) into the manager; the bridge
|
|
62
|
+
* forwards the derived motion scale to every attached target.
|
|
63
|
+
*
|
|
64
|
+
* Extracted from Pan Tvardowski per Phase 11.14. Duck-typed on the target
|
|
65
|
+
* side so `@flare-engine/a11y` stays dependency-free.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/** Minimal structural contract. `Camera2D` and `PostFxController` both match. */
|
|
69
|
+
interface MotionScaleTarget {
|
|
70
|
+
setMotionScale(k: number): void;
|
|
71
|
+
}
|
|
72
|
+
interface A11yBridge {
|
|
73
|
+
readonly manager: A11yManager;
|
|
74
|
+
/**
|
|
75
|
+
* Apply the current motion scale to `target` and subscribe for updates.
|
|
76
|
+
* Returns an unsubscribe fn — call on target disposal.
|
|
77
|
+
*/
|
|
78
|
+
attach(target: MotionScaleTarget): () => void;
|
|
79
|
+
/** Alias of `attach` with a name that documents intent at call sites. */
|
|
80
|
+
attachCamera(camera: MotionScaleTarget): () => void;
|
|
81
|
+
/** Alias of `attach` with a name that documents intent at call sites. */
|
|
82
|
+
attachPostfx(postfx: MotionScaleTarget): () => void;
|
|
83
|
+
}
|
|
84
|
+
interface A11yBridgeOptions extends A11yManagerOptions {
|
|
85
|
+
/** Provide an existing manager — otherwise one is constructed from `initial`. */
|
|
86
|
+
readonly manager?: A11yManager;
|
|
87
|
+
}
|
|
88
|
+
declare function createA11yBridge(options?: A11yBridgeOptions): A11yBridge;
|
|
89
|
+
|
|
90
|
+
export { type A11yBridge, type A11yBridgeOptions, A11yManager, type A11yManagerOptions, type A11ySettings, type ContrastLevel, type MotionScaleTarget, createA11yBridge, dampenedMotion, motionScale, pickContrast };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Signal } from '@flare-engine/events';
|
|
2
|
+
|
|
3
|
+
type ContrastLevel = 'normal' | 'high';
|
|
4
|
+
interface A11ySettings {
|
|
5
|
+
/** When true, animations and heavy postfx should be minimized. */
|
|
6
|
+
reducedMotion: boolean;
|
|
7
|
+
/** Current contrast preference — UI tokens swap accordingly. */
|
|
8
|
+
contrast: ContrastLevel;
|
|
9
|
+
/** Global UI scale multiplier (`1` = default). */
|
|
10
|
+
uiScale: number;
|
|
11
|
+
}
|
|
12
|
+
interface A11yManagerOptions {
|
|
13
|
+
initial?: Partial<A11ySettings>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Central accessibility state. Systems subscribe via `onChange` and read the
|
|
17
|
+
* current snapshot via `settings`. The manager owns no platform wiring — a
|
|
18
|
+
* host app may bridge OS preferences (e.g., `AccessibilityInfo.isReduceMotionEnabled`)
|
|
19
|
+
* by pushing values in on startup.
|
|
20
|
+
*/
|
|
21
|
+
declare class A11yManager {
|
|
22
|
+
readonly onChange: Signal<A11ySettings>;
|
|
23
|
+
private _settings;
|
|
24
|
+
constructor(options?: A11yManagerOptions);
|
|
25
|
+
get settings(): A11ySettings;
|
|
26
|
+
get reducedMotion(): boolean;
|
|
27
|
+
get contrast(): ContrastLevel;
|
|
28
|
+
get uiScale(): number;
|
|
29
|
+
update(patch: Partial<A11ySettings>): void;
|
|
30
|
+
setReducedMotion(v: boolean): void;
|
|
31
|
+
setContrast(v: ContrastLevel): void;
|
|
32
|
+
setUiScale(v: number): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Strength multiplier used by animation + postfx systems. Caller chooses a
|
|
37
|
+
* base intensity; this function clamps it for reduced-motion users.
|
|
38
|
+
*/
|
|
39
|
+
declare function motionScale(settings: {
|
|
40
|
+
reducedMotion: boolean;
|
|
41
|
+
}, base?: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* Returns a second, dampened intensity. Useful for effects you want to keep
|
|
44
|
+
* but soften (camera shake, chromatic aberration). `reducedFactor` controls
|
|
45
|
+
* how much survives when reduced-motion is on (default 20%).
|
|
46
|
+
*/
|
|
47
|
+
declare function dampenedMotion(settings: {
|
|
48
|
+
reducedMotion: boolean;
|
|
49
|
+
}, base: number, reducedFactor?: number): number;
|
|
50
|
+
/**
|
|
51
|
+
* Helper for UI layers: pick a color based on contrast preference.
|
|
52
|
+
*/
|
|
53
|
+
declare function pickContrast<T>(settings: Pick<A11ySettings, 'contrast'>, normal: T, high: T): T;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A11yBridge — one-liner wiring of an `A11yManager` into runtime systems that
|
|
57
|
+
* expose `setMotionScale(k)`. Typical targets: `Camera2D` (screen-shake) and
|
|
58
|
+
* `PostFxController` (global effect intensity).
|
|
59
|
+
*
|
|
60
|
+
* The bridge owns no platform wiring — callers push OS preferences (e.g.
|
|
61
|
+
* `AccessibilityInfo.isReduceMotionEnabled()`) into the manager; the bridge
|
|
62
|
+
* forwards the derived motion scale to every attached target.
|
|
63
|
+
*
|
|
64
|
+
* Extracted from Pan Tvardowski per Phase 11.14. Duck-typed on the target
|
|
65
|
+
* side so `@flare-engine/a11y` stays dependency-free.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/** Minimal structural contract. `Camera2D` and `PostFxController` both match. */
|
|
69
|
+
interface MotionScaleTarget {
|
|
70
|
+
setMotionScale(k: number): void;
|
|
71
|
+
}
|
|
72
|
+
interface A11yBridge {
|
|
73
|
+
readonly manager: A11yManager;
|
|
74
|
+
/**
|
|
75
|
+
* Apply the current motion scale to `target` and subscribe for updates.
|
|
76
|
+
* Returns an unsubscribe fn — call on target disposal.
|
|
77
|
+
*/
|
|
78
|
+
attach(target: MotionScaleTarget): () => void;
|
|
79
|
+
/** Alias of `attach` with a name that documents intent at call sites. */
|
|
80
|
+
attachCamera(camera: MotionScaleTarget): () => void;
|
|
81
|
+
/** Alias of `attach` with a name that documents intent at call sites. */
|
|
82
|
+
attachPostfx(postfx: MotionScaleTarget): () => void;
|
|
83
|
+
}
|
|
84
|
+
interface A11yBridgeOptions extends A11yManagerOptions {
|
|
85
|
+
/** Provide an existing manager — otherwise one is constructed from `initial`. */
|
|
86
|
+
readonly manager?: A11yManager;
|
|
87
|
+
}
|
|
88
|
+
declare function createA11yBridge(options?: A11yBridgeOptions): A11yBridge;
|
|
89
|
+
|
|
90
|
+
export { type A11yBridge, type A11yBridgeOptions, A11yManager, type A11yManagerOptions, type A11ySettings, type ContrastLevel, type MotionScaleTarget, createA11yBridge, dampenedMotion, motionScale, pickContrast };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
A11yManager: () => A11yManager,
|
|
24
|
+
createA11yBridge: () => createA11yBridge,
|
|
25
|
+
dampenedMotion: () => dampenedMotion,
|
|
26
|
+
motionScale: () => motionScale,
|
|
27
|
+
pickContrast: () => pickContrast
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/manager.ts
|
|
32
|
+
var import_events = require("@flare-engine/events");
|
|
33
|
+
var DEFAULTS = {
|
|
34
|
+
reducedMotion: false,
|
|
35
|
+
contrast: "normal",
|
|
36
|
+
uiScale: 1
|
|
37
|
+
};
|
|
38
|
+
var A11yManager = class {
|
|
39
|
+
onChange = new import_events.Signal();
|
|
40
|
+
_settings;
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this._settings = { ...DEFAULTS, ...options.initial };
|
|
43
|
+
}
|
|
44
|
+
get settings() {
|
|
45
|
+
return this._settings;
|
|
46
|
+
}
|
|
47
|
+
get reducedMotion() {
|
|
48
|
+
return this._settings.reducedMotion;
|
|
49
|
+
}
|
|
50
|
+
get contrast() {
|
|
51
|
+
return this._settings.contrast;
|
|
52
|
+
}
|
|
53
|
+
get uiScale() {
|
|
54
|
+
return this._settings.uiScale;
|
|
55
|
+
}
|
|
56
|
+
update(patch) {
|
|
57
|
+
const next = { ...this._settings, ...patch };
|
|
58
|
+
if (next.reducedMotion === this._settings.reducedMotion && next.contrast === this._settings.contrast && next.uiScale === this._settings.uiScale) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this._settings = next;
|
|
62
|
+
this.onChange.emit(next);
|
|
63
|
+
}
|
|
64
|
+
setReducedMotion(v) {
|
|
65
|
+
this.update({ reducedMotion: v });
|
|
66
|
+
}
|
|
67
|
+
setContrast(v) {
|
|
68
|
+
this.update({ contrast: v });
|
|
69
|
+
}
|
|
70
|
+
setUiScale(v) {
|
|
71
|
+
this.update({ uiScale: v });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/helpers.ts
|
|
76
|
+
function motionScale(settings, base = 1) {
|
|
77
|
+
return settings.reducedMotion ? 0 : base;
|
|
78
|
+
}
|
|
79
|
+
function dampenedMotion(settings, base, reducedFactor = 0.2) {
|
|
80
|
+
return settings.reducedMotion ? base * reducedFactor : base;
|
|
81
|
+
}
|
|
82
|
+
function pickContrast(settings, normal, high) {
|
|
83
|
+
return settings.contrast === "high" ? high : normal;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/bridge.ts
|
|
87
|
+
function createA11yBridge(options = {}) {
|
|
88
|
+
const manager = options.manager ?? new A11yManager({ initial: options.initial });
|
|
89
|
+
function attach(target) {
|
|
90
|
+
const apply = () => {
|
|
91
|
+
target.setMotionScale(motionScale(manager.settings));
|
|
92
|
+
};
|
|
93
|
+
apply();
|
|
94
|
+
manager.onChange.add(apply);
|
|
95
|
+
return () => manager.onChange.remove(apply);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
manager,
|
|
99
|
+
attach,
|
|
100
|
+
attachCamera: attach,
|
|
101
|
+
attachPostfx: attach
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
105
|
+
0 && (module.exports = {
|
|
106
|
+
A11yManager,
|
|
107
|
+
createA11yBridge,
|
|
108
|
+
dampenedMotion,
|
|
109
|
+
motionScale,
|
|
110
|
+
pickContrast
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/manager.ts","../src/helpers.ts","../src/bridge.ts"],"sourcesContent":["export {\n A11yManager,\n type A11yManagerOptions,\n type A11ySettings,\n type ContrastLevel,\n} from './manager.js';\nexport { motionScale, dampenedMotion, pickContrast } from './helpers.js';\nexport {\n type A11yBridge,\n type A11yBridgeOptions,\n type MotionScaleTarget,\n createA11yBridge,\n} from './bridge.js';\n","import { Signal } from '@flare-engine/events';\n\nexport type ContrastLevel = 'normal' | 'high';\n\nexport interface A11ySettings {\n /** When true, animations and heavy postfx should be minimized. */\n reducedMotion: boolean;\n /** Current contrast preference — UI tokens swap accordingly. */\n contrast: ContrastLevel;\n /** Global UI scale multiplier (`1` = default). */\n uiScale: number;\n}\n\nexport interface A11yManagerOptions {\n initial?: Partial<A11ySettings>;\n}\n\nconst DEFAULTS: A11ySettings = {\n reducedMotion: false,\n contrast: 'normal',\n uiScale: 1,\n};\n\n/**\n * Central accessibility state. Systems subscribe via `onChange` and read the\n * current snapshot via `settings`. The manager owns no platform wiring — a\n * host app may bridge OS preferences (e.g., `AccessibilityInfo.isReduceMotionEnabled`)\n * by pushing values in on startup.\n */\nexport class A11yManager {\n readonly onChange = new Signal<A11ySettings>();\n private _settings: A11ySettings;\n\n constructor(options: A11yManagerOptions = {}) {\n this._settings = { ...DEFAULTS, ...options.initial };\n }\n\n get settings(): A11ySettings {\n return this._settings;\n }\n\n get reducedMotion(): boolean {\n return this._settings.reducedMotion;\n }\n\n get contrast(): ContrastLevel {\n return this._settings.contrast;\n }\n\n get uiScale(): number {\n return this._settings.uiScale;\n }\n\n update(patch: Partial<A11ySettings>): void {\n const next: A11ySettings = { ...this._settings, ...patch };\n if (\n next.reducedMotion === this._settings.reducedMotion &&\n next.contrast === this._settings.contrast &&\n next.uiScale === this._settings.uiScale\n ) {\n return;\n }\n this._settings = next;\n this.onChange.emit(next);\n }\n\n setReducedMotion(v: boolean): void {\n this.update({ reducedMotion: v });\n }\n\n setContrast(v: ContrastLevel): void {\n this.update({ contrast: v });\n }\n\n setUiScale(v: number): void {\n this.update({ uiScale: v });\n }\n}\n","import type { A11ySettings } from './manager.js';\n\n/**\n * Strength multiplier used by animation + postfx systems. Caller chooses a\n * base intensity; this function clamps it for reduced-motion users.\n */\nexport function motionScale(settings: { reducedMotion: boolean }, base = 1): number {\n return settings.reducedMotion ? 0 : base;\n}\n\n/**\n * Returns a second, dampened intensity. Useful for effects you want to keep\n * but soften (camera shake, chromatic aberration). `reducedFactor` controls\n * how much survives when reduced-motion is on (default 20%).\n */\nexport function dampenedMotion(\n settings: { reducedMotion: boolean },\n base: number,\n reducedFactor = 0.2,\n): number {\n return settings.reducedMotion ? base * reducedFactor : base;\n}\n\n/**\n * Helper for UI layers: pick a color based on contrast preference.\n */\nexport function pickContrast<T>(settings: Pick<A11ySettings, 'contrast'>, normal: T, high: T): T {\n return settings.contrast === 'high' ? high : normal;\n}\n","/**\n * A11yBridge — one-liner wiring of an `A11yManager` into runtime systems that\n * expose `setMotionScale(k)`. Typical targets: `Camera2D` (screen-shake) and\n * `PostFxController` (global effect intensity).\n *\n * The bridge owns no platform wiring — callers push OS preferences (e.g.\n * `AccessibilityInfo.isReduceMotionEnabled()`) into the manager; the bridge\n * forwards the derived motion scale to every attached target.\n *\n * Extracted from Pan Tvardowski per Phase 11.14. Duck-typed on the target\n * side so `@flare-engine/a11y` stays dependency-free.\n */\n\nimport { motionScale } from './helpers.js';\nimport { A11yManager, type A11yManagerOptions } from './manager.js';\n\n/** Minimal structural contract. `Camera2D` and `PostFxController` both match. */\nexport interface MotionScaleTarget {\n setMotionScale(k: number): void;\n}\n\nexport interface A11yBridge {\n readonly manager: A11yManager;\n /**\n * Apply the current motion scale to `target` and subscribe for updates.\n * Returns an unsubscribe fn — call on target disposal.\n */\n attach(target: MotionScaleTarget): () => void;\n /** Alias of `attach` with a name that documents intent at call sites. */\n attachCamera(camera: MotionScaleTarget): () => void;\n /** Alias of `attach` with a name that documents intent at call sites. */\n attachPostfx(postfx: MotionScaleTarget): () => void;\n}\n\nexport interface A11yBridgeOptions extends A11yManagerOptions {\n /** Provide an existing manager — otherwise one is constructed from `initial`. */\n readonly manager?: A11yManager;\n}\n\nexport function createA11yBridge(options: A11yBridgeOptions = {}): A11yBridge {\n const manager = options.manager ?? new A11yManager({ initial: options.initial });\n\n function attach(target: MotionScaleTarget): () => void {\n const apply = (): void => {\n target.setMotionScale(motionScale(manager.settings));\n };\n apply();\n manager.onChange.add(apply);\n return () => manager.onChange.remove(apply);\n }\n\n return {\n manager,\n attach,\n attachCamera: attach,\n attachPostfx: attach,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,oBAAuB;AAiBvB,IAAM,WAAyB;AAAA,EAC7B,eAAe;AAAA,EACf,UAAU;AAAA,EACV,SAAS;AACX;AAQO,IAAM,cAAN,MAAkB;AAAA,EACd,WAAW,IAAI,qBAAqB;AAAA,EACrC;AAAA,EAER,YAAY,UAA8B,CAAC,GAAG;AAC5C,SAAK,YAAY,EAAE,GAAG,UAAU,GAAG,QAAQ,QAAQ;AAAA,EACrD;AAAA,EAEA,IAAI,WAAyB;AAC3B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,gBAAyB;AAC3B,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,IAAI,WAA0B;AAC5B,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,IAAI,UAAkB;AACpB,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,OAAO,OAAoC;AACzC,UAAM,OAAqB,EAAE,GAAG,KAAK,WAAW,GAAG,MAAM;AACzD,QACE,KAAK,kBAAkB,KAAK,UAAU,iBACtC,KAAK,aAAa,KAAK,UAAU,YACjC,KAAK,YAAY,KAAK,UAAU,SAChC;AACA;AAAA,IACF;AACA,SAAK,YAAY;AACjB,SAAK,SAAS,KAAK,IAAI;AAAA,EACzB;AAAA,EAEA,iBAAiB,GAAkB;AACjC,SAAK,OAAO,EAAE,eAAe,EAAE,CAAC;AAAA,EAClC;AAAA,EAEA,YAAY,GAAwB;AAClC,SAAK,OAAO,EAAE,UAAU,EAAE,CAAC;AAAA,EAC7B;AAAA,EAEA,WAAW,GAAiB;AAC1B,SAAK,OAAO,EAAE,SAAS,EAAE,CAAC;AAAA,EAC5B;AACF;;;ACvEO,SAAS,YAAY,UAAsC,OAAO,GAAW;AAClF,SAAO,SAAS,gBAAgB,IAAI;AACtC;AAOO,SAAS,eACd,UACA,MACA,gBAAgB,KACR;AACR,SAAO,SAAS,gBAAgB,OAAO,gBAAgB;AACzD;AAKO,SAAS,aAAgB,UAA0C,QAAW,MAAY;AAC/F,SAAO,SAAS,aAAa,SAAS,OAAO;AAC/C;;;ACWO,SAAS,iBAAiB,UAA6B,CAAC,GAAe;AAC5E,QAAM,UAAU,QAAQ,WAAW,IAAI,YAAY,EAAE,SAAS,QAAQ,QAAQ,CAAC;AAE/E,WAAS,OAAO,QAAuC;AACrD,UAAM,QAAQ,MAAY;AACxB,aAAO,eAAe,YAAY,QAAQ,QAAQ,CAAC;AAAA,IACrD;AACA,UAAM;AACN,YAAQ,SAAS,IAAI,KAAK;AAC1B,WAAO,MAAM,QAAQ,SAAS,OAAO,KAAK;AAAA,EAC5C;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,cAAc;AAAA,EAChB;AACF;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/manager.ts
|
|
2
|
+
import { Signal } from "@flare-engine/events";
|
|
3
|
+
var DEFAULTS = {
|
|
4
|
+
reducedMotion: false,
|
|
5
|
+
contrast: "normal",
|
|
6
|
+
uiScale: 1
|
|
7
|
+
};
|
|
8
|
+
var A11yManager = class {
|
|
9
|
+
onChange = new Signal();
|
|
10
|
+
_settings;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this._settings = { ...DEFAULTS, ...options.initial };
|
|
13
|
+
}
|
|
14
|
+
get settings() {
|
|
15
|
+
return this._settings;
|
|
16
|
+
}
|
|
17
|
+
get reducedMotion() {
|
|
18
|
+
return this._settings.reducedMotion;
|
|
19
|
+
}
|
|
20
|
+
get contrast() {
|
|
21
|
+
return this._settings.contrast;
|
|
22
|
+
}
|
|
23
|
+
get uiScale() {
|
|
24
|
+
return this._settings.uiScale;
|
|
25
|
+
}
|
|
26
|
+
update(patch) {
|
|
27
|
+
const next = { ...this._settings, ...patch };
|
|
28
|
+
if (next.reducedMotion === this._settings.reducedMotion && next.contrast === this._settings.contrast && next.uiScale === this._settings.uiScale) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this._settings = next;
|
|
32
|
+
this.onChange.emit(next);
|
|
33
|
+
}
|
|
34
|
+
setReducedMotion(v) {
|
|
35
|
+
this.update({ reducedMotion: v });
|
|
36
|
+
}
|
|
37
|
+
setContrast(v) {
|
|
38
|
+
this.update({ contrast: v });
|
|
39
|
+
}
|
|
40
|
+
setUiScale(v) {
|
|
41
|
+
this.update({ uiScale: v });
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/helpers.ts
|
|
46
|
+
function motionScale(settings, base = 1) {
|
|
47
|
+
return settings.reducedMotion ? 0 : base;
|
|
48
|
+
}
|
|
49
|
+
function dampenedMotion(settings, base, reducedFactor = 0.2) {
|
|
50
|
+
return settings.reducedMotion ? base * reducedFactor : base;
|
|
51
|
+
}
|
|
52
|
+
function pickContrast(settings, normal, high) {
|
|
53
|
+
return settings.contrast === "high" ? high : normal;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/bridge.ts
|
|
57
|
+
function createA11yBridge(options = {}) {
|
|
58
|
+
const manager = options.manager ?? new A11yManager({ initial: options.initial });
|
|
59
|
+
function attach(target) {
|
|
60
|
+
const apply = () => {
|
|
61
|
+
target.setMotionScale(motionScale(manager.settings));
|
|
62
|
+
};
|
|
63
|
+
apply();
|
|
64
|
+
manager.onChange.add(apply);
|
|
65
|
+
return () => manager.onChange.remove(apply);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
manager,
|
|
69
|
+
attach,
|
|
70
|
+
attachCamera: attach,
|
|
71
|
+
attachPostfx: attach
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
A11yManager,
|
|
76
|
+
createA11yBridge,
|
|
77
|
+
dampenedMotion,
|
|
78
|
+
motionScale,
|
|
79
|
+
pickContrast
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/manager.ts","../src/helpers.ts","../src/bridge.ts"],"sourcesContent":["import { Signal } from '@flare-engine/events';\n\nexport type ContrastLevel = 'normal' | 'high';\n\nexport interface A11ySettings {\n /** When true, animations and heavy postfx should be minimized. */\n reducedMotion: boolean;\n /** Current contrast preference — UI tokens swap accordingly. */\n contrast: ContrastLevel;\n /** Global UI scale multiplier (`1` = default). */\n uiScale: number;\n}\n\nexport interface A11yManagerOptions {\n initial?: Partial<A11ySettings>;\n}\n\nconst DEFAULTS: A11ySettings = {\n reducedMotion: false,\n contrast: 'normal',\n uiScale: 1,\n};\n\n/**\n * Central accessibility state. Systems subscribe via `onChange` and read the\n * current snapshot via `settings`. The manager owns no platform wiring — a\n * host app may bridge OS preferences (e.g., `AccessibilityInfo.isReduceMotionEnabled`)\n * by pushing values in on startup.\n */\nexport class A11yManager {\n readonly onChange = new Signal<A11ySettings>();\n private _settings: A11ySettings;\n\n constructor(options: A11yManagerOptions = {}) {\n this._settings = { ...DEFAULTS, ...options.initial };\n }\n\n get settings(): A11ySettings {\n return this._settings;\n }\n\n get reducedMotion(): boolean {\n return this._settings.reducedMotion;\n }\n\n get contrast(): ContrastLevel {\n return this._settings.contrast;\n }\n\n get uiScale(): number {\n return this._settings.uiScale;\n }\n\n update(patch: Partial<A11ySettings>): void {\n const next: A11ySettings = { ...this._settings, ...patch };\n if (\n next.reducedMotion === this._settings.reducedMotion &&\n next.contrast === this._settings.contrast &&\n next.uiScale === this._settings.uiScale\n ) {\n return;\n }\n this._settings = next;\n this.onChange.emit(next);\n }\n\n setReducedMotion(v: boolean): void {\n this.update({ reducedMotion: v });\n }\n\n setContrast(v: ContrastLevel): void {\n this.update({ contrast: v });\n }\n\n setUiScale(v: number): void {\n this.update({ uiScale: v });\n }\n}\n","import type { A11ySettings } from './manager.js';\n\n/**\n * Strength multiplier used by animation + postfx systems. Caller chooses a\n * base intensity; this function clamps it for reduced-motion users.\n */\nexport function motionScale(settings: { reducedMotion: boolean }, base = 1): number {\n return settings.reducedMotion ? 0 : base;\n}\n\n/**\n * Returns a second, dampened intensity. Useful for effects you want to keep\n * but soften (camera shake, chromatic aberration). `reducedFactor` controls\n * how much survives when reduced-motion is on (default 20%).\n */\nexport function dampenedMotion(\n settings: { reducedMotion: boolean },\n base: number,\n reducedFactor = 0.2,\n): number {\n return settings.reducedMotion ? base * reducedFactor : base;\n}\n\n/**\n * Helper for UI layers: pick a color based on contrast preference.\n */\nexport function pickContrast<T>(settings: Pick<A11ySettings, 'contrast'>, normal: T, high: T): T {\n return settings.contrast === 'high' ? high : normal;\n}\n","/**\n * A11yBridge — one-liner wiring of an `A11yManager` into runtime systems that\n * expose `setMotionScale(k)`. Typical targets: `Camera2D` (screen-shake) and\n * `PostFxController` (global effect intensity).\n *\n * The bridge owns no platform wiring — callers push OS preferences (e.g.\n * `AccessibilityInfo.isReduceMotionEnabled()`) into the manager; the bridge\n * forwards the derived motion scale to every attached target.\n *\n * Extracted from Pan Tvardowski per Phase 11.14. Duck-typed on the target\n * side so `@flare-engine/a11y` stays dependency-free.\n */\n\nimport { motionScale } from './helpers.js';\nimport { A11yManager, type A11yManagerOptions } from './manager.js';\n\n/** Minimal structural contract. `Camera2D` and `PostFxController` both match. */\nexport interface MotionScaleTarget {\n setMotionScale(k: number): void;\n}\n\nexport interface A11yBridge {\n readonly manager: A11yManager;\n /**\n * Apply the current motion scale to `target` and subscribe for updates.\n * Returns an unsubscribe fn — call on target disposal.\n */\n attach(target: MotionScaleTarget): () => void;\n /** Alias of `attach` with a name that documents intent at call sites. */\n attachCamera(camera: MotionScaleTarget): () => void;\n /** Alias of `attach` with a name that documents intent at call sites. */\n attachPostfx(postfx: MotionScaleTarget): () => void;\n}\n\nexport interface A11yBridgeOptions extends A11yManagerOptions {\n /** Provide an existing manager — otherwise one is constructed from `initial`. */\n readonly manager?: A11yManager;\n}\n\nexport function createA11yBridge(options: A11yBridgeOptions = {}): A11yBridge {\n const manager = options.manager ?? new A11yManager({ initial: options.initial });\n\n function attach(target: MotionScaleTarget): () => void {\n const apply = (): void => {\n target.setMotionScale(motionScale(manager.settings));\n };\n apply();\n manager.onChange.add(apply);\n return () => manager.onChange.remove(apply);\n }\n\n return {\n manager,\n attach,\n attachCamera: attach,\n attachPostfx: attach,\n };\n}\n"],"mappings":";AAAA,SAAS,cAAc;AAiBvB,IAAM,WAAyB;AAAA,EAC7B,eAAe;AAAA,EACf,UAAU;AAAA,EACV,SAAS;AACX;AAQO,IAAM,cAAN,MAAkB;AAAA,EACd,WAAW,IAAI,OAAqB;AAAA,EACrC;AAAA,EAER,YAAY,UAA8B,CAAC,GAAG;AAC5C,SAAK,YAAY,EAAE,GAAG,UAAU,GAAG,QAAQ,QAAQ;AAAA,EACrD;AAAA,EAEA,IAAI,WAAyB;AAC3B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,gBAAyB;AAC3B,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,IAAI,WAA0B;AAC5B,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,IAAI,UAAkB;AACpB,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,OAAO,OAAoC;AACzC,UAAM,OAAqB,EAAE,GAAG,KAAK,WAAW,GAAG,MAAM;AACzD,QACE,KAAK,kBAAkB,KAAK,UAAU,iBACtC,KAAK,aAAa,KAAK,UAAU,YACjC,KAAK,YAAY,KAAK,UAAU,SAChC;AACA;AAAA,IACF;AACA,SAAK,YAAY;AACjB,SAAK,SAAS,KAAK,IAAI;AAAA,EACzB;AAAA,EAEA,iBAAiB,GAAkB;AACjC,SAAK,OAAO,EAAE,eAAe,EAAE,CAAC;AAAA,EAClC;AAAA,EAEA,YAAY,GAAwB;AAClC,SAAK,OAAO,EAAE,UAAU,EAAE,CAAC;AAAA,EAC7B;AAAA,EAEA,WAAW,GAAiB;AAC1B,SAAK,OAAO,EAAE,SAAS,EAAE,CAAC;AAAA,EAC5B;AACF;;;ACvEO,SAAS,YAAY,UAAsC,OAAO,GAAW;AAClF,SAAO,SAAS,gBAAgB,IAAI;AACtC;AAOO,SAAS,eACd,UACA,MACA,gBAAgB,KACR;AACR,SAAO,SAAS,gBAAgB,OAAO,gBAAgB;AACzD;AAKO,SAAS,aAAgB,UAA0C,QAAW,MAAY;AAC/F,SAAO,SAAS,aAAa,SAAS,OAAO;AAC/C;;;ACWO,SAAS,iBAAiB,UAA6B,CAAC,GAAe;AAC5E,QAAM,UAAU,QAAQ,WAAW,IAAI,YAAY,EAAE,SAAS,QAAQ,QAAQ,CAAC;AAE/E,WAAS,OAAO,QAAuC;AACrD,UAAM,QAAQ,MAAY;AACxB,aAAO,eAAe,YAAY,QAAQ,QAAQ,CAAC;AAAA,IACrD;AACA,UAAM;AACN,YAAQ,SAAS,IAAI,KAAK;AAC1B,WAAO,MAAM,QAAQ,SAAS,OAAO,KAAK;AAAA,EAC5C;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,cAAc;AAAA,EAChB;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flare-engine/a11y",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Accessibility primitives — reduced motion, contrast tokens",
|
|
5
|
+
"keywords": ["flare-engine", "a11y", "accessibility", "reduced-motion"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Grzegorz Otto",
|
|
8
|
+
"homepage": "https://github.com/grzott/flare-engine#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/grzott/flare-engine/issues"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/grzott/flare-engine.git",
|
|
15
|
+
"directory": "packages/a11y"
|
|
16
|
+
},
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"module": "dist/index.mjs",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.mjs",
|
|
24
|
+
"require": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"test": "bun test",
|
|
31
|
+
"lint": "biome check src/",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@flare-engine/events": "^0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "restricted",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"sideEffects": false
|
|
42
|
+
}
|