@ai11y/core 0.0.1
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/.turbo/turbo-build.log +87 -0
- package/CHANGELOG.md +29 -0
- package/README.md +37 -0
- package/dist/agent/agent-adapter.d.mts +17 -0
- package/dist/agent/agent-adapter.d.mts.map +1 -0
- package/dist/agent/agent-adapter.mjs +45 -0
- package/dist/agent/agent-adapter.mjs.map +1 -0
- package/dist/agent/llm-agent.d.mts +13 -0
- package/dist/agent/llm-agent.d.mts.map +1 -0
- package/dist/agent/llm-agent.mjs +41 -0
- package/dist/agent/llm-agent.mjs.map +1 -0
- package/dist/agent/plan.d.mts +27 -0
- package/dist/agent/plan.d.mts.map +1 -0
- package/dist/agent/plan.mjs +28 -0
- package/dist/agent/plan.mjs.map +1 -0
- package/dist/agent/rule-based-agent.d.mts +13 -0
- package/dist/agent/rule-based-agent.d.mts.map +1 -0
- package/dist/agent/rule-based-agent.mjs +152 -0
- package/dist/agent/rule-based-agent.mjs.map +1 -0
- package/dist/agent/tool-contract.d.mts +19 -0
- package/dist/agent/tool-contract.d.mts.map +1 -0
- package/dist/agent/types.d.mts +77 -0
- package/dist/agent/types.d.mts.map +1 -0
- package/dist/client-api.d.mts +49 -0
- package/dist/client-api.d.mts.map +1 -0
- package/dist/client-api.mjs +68 -0
- package/dist/client-api.mjs.map +1 -0
- package/dist/context.d.mts +29 -0
- package/dist/context.d.mts.map +1 -0
- package/dist/dom-actions/click.d.mts +15 -0
- package/dist/dom-actions/click.d.mts.map +1 -0
- package/dist/dom-actions/click.mjs +36 -0
- package/dist/dom-actions/click.mjs.map +1 -0
- package/dist/dom-actions/fill-input.d.mts +17 -0
- package/dist/dom-actions/fill-input.d.mts.map +1 -0
- package/dist/dom-actions/fill-input.mjs +69 -0
- package/dist/dom-actions/fill-input.mjs.map +1 -0
- package/dist/dom-actions/find-element.mjs +17 -0
- package/dist/dom-actions/find-element.mjs.map +1 -0
- package/dist/dom-actions/highlight.d.mts +34 -0
- package/dist/dom-actions/highlight.d.mts.map +1 -0
- package/dist/dom-actions/highlight.mjs +60 -0
- package/dist/dom-actions/highlight.mjs.map +1 -0
- package/dist/dom-actions/navigate.d.mts +16 -0
- package/dist/dom-actions/navigate.d.mts.map +1 -0
- package/dist/dom-actions/navigate.mjs +22 -0
- package/dist/dom-actions/navigate.mjs.map +1 -0
- package/dist/dom-actions/scroll.d.mts +16 -0
- package/dist/dom-actions/scroll.d.mts.map +1 -0
- package/dist/dom-actions/scroll.mjs +32 -0
- package/dist/dom-actions/scroll.mjs.map +1 -0
- package/dist/dom.d.mts +23 -0
- package/dist/dom.d.mts.map +1 -0
- package/dist/dom.mjs +60 -0
- package/dist/dom.mjs.map +1 -0
- package/dist/events.d.mts +35 -0
- package/dist/events.d.mts.map +1 -0
- package/dist/events.mjs +49 -0
- package/dist/events.mjs.map +1 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.mjs +18 -0
- package/dist/instruction.d.mts +21 -0
- package/dist/instruction.d.mts.map +1 -0
- package/dist/marker.d.mts +35 -0
- package/dist/marker.d.mts.map +1 -0
- package/dist/marker.mjs +137 -0
- package/dist/marker.mjs.map +1 -0
- package/dist/store.d.mts +56 -0
- package/dist/store.d.mts.map +1 -0
- package/dist/store.mjs +114 -0
- package/dist/store.mjs.map +1 -0
- package/dist/util/attributes.d.mts +57 -0
- package/dist/util/attributes.d.mts.map +1 -0
- package/dist/util/attributes.mjs +68 -0
- package/dist/util/attributes.mjs.map +1 -0
- package/dist/util/format.d.mts +18 -0
- package/dist/util/format.d.mts.map +1 -0
- package/dist/util/format.mjs +21 -0
- package/dist/util/format.mjs.map +1 -0
- package/package.json +26 -0
- package/src/agent/agent-adapter.ts +75 -0
- package/src/agent/index.ts +21 -0
- package/src/agent/llm-agent.ts +64 -0
- package/src/agent/plan.ts +41 -0
- package/src/agent/rule-based-agent.ts +269 -0
- package/src/agent/tool-contract.ts +22 -0
- package/src/agent/types.ts +83 -0
- package/src/client-api.ts +107 -0
- package/src/context.ts +28 -0
- package/src/dom-actions/click.ts +39 -0
- package/src/dom-actions/fill-input.ts +113 -0
- package/src/dom-actions/find-element.ts +14 -0
- package/src/dom-actions/highlight.ts +93 -0
- package/src/dom-actions/index.ts +5 -0
- package/src/dom-actions/navigate.ts +17 -0
- package/src/dom-actions/scroll.ts +29 -0
- package/src/dom.ts +89 -0
- package/src/events.ts +55 -0
- package/src/index.ts +55 -0
- package/src/instruction.ts +6 -0
- package/src/marker.ts +237 -0
- package/src/store.ts +138 -0
- package/src/util/attributes.ts +68 -0
- package/src/util/format.ts +16 -0
- package/src/util/index.ts +2 -0
- package/tsconfig.json +18 -0
- package/tsdown.config.ts +10 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Ai11yContext } from "../context.js";
|
|
2
|
+
|
|
3
|
+
export interface ToolDefinition {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
parameters: {
|
|
7
|
+
type: "object";
|
|
8
|
+
properties: Record<
|
|
9
|
+
string,
|
|
10
|
+
{
|
|
11
|
+
type: string;
|
|
12
|
+
description: string;
|
|
13
|
+
}
|
|
14
|
+
>;
|
|
15
|
+
required?: string[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ToolExecutor = (
|
|
20
|
+
args: Record<string, unknown>,
|
|
21
|
+
context: Ai11yContext,
|
|
22
|
+
) => Promise<unknown> | unknown;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Ai11yContext } from "../context.js";
|
|
2
|
+
import type { Instruction } from "../instruction.js";
|
|
3
|
+
|
|
4
|
+
export interface AgentResponse {
|
|
5
|
+
reply: string;
|
|
6
|
+
instructions?: Instruction[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ConversationMessage {
|
|
10
|
+
role: "user" | "assistant";
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AgentRequest {
|
|
15
|
+
input: string;
|
|
16
|
+
context: Ai11yContext;
|
|
17
|
+
messages?: ConversationMessage[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for LLM-based agent
|
|
22
|
+
*/
|
|
23
|
+
export interface LLMAgentConfig {
|
|
24
|
+
/**
|
|
25
|
+
* API endpoint URL for the agent server (e.g., "http://localhost:3000/ai11y/agent")
|
|
26
|
+
*/
|
|
27
|
+
apiEndpoint: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Agent mode selection
|
|
32
|
+
*/
|
|
33
|
+
export type AgentMode = "llm" | "rule-based" | "auto";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Configuration for agent selection
|
|
37
|
+
*/
|
|
38
|
+
export interface AgentAdapterConfig {
|
|
39
|
+
/**
|
|
40
|
+
* Agent mode selection strategy
|
|
41
|
+
* - "llm": Always use LLM agent (requires llmConfig)
|
|
42
|
+
* - "rule-based": Always use rule-based agent
|
|
43
|
+
* - "auto": Use LLM if configured, fallback to rule-based on error or when offline
|
|
44
|
+
* @default "auto"
|
|
45
|
+
*/
|
|
46
|
+
mode?: AgentMode;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* LLM configuration (required for "llm" mode, optional for "auto")
|
|
50
|
+
*/
|
|
51
|
+
llmConfig?: LLMAgentConfig | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Force rule-based mode (skip LLM even if config is available)
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
forceRuleBased?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Unified agent configuration combining LLM settings with agent selection options
|
|
62
|
+
*/
|
|
63
|
+
export interface AgentConfig {
|
|
64
|
+
/**
|
|
65
|
+
* API endpoint URL for the LLM agent server (required for "llm" mode)
|
|
66
|
+
*/
|
|
67
|
+
apiEndpoint?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Agent mode selection strategy
|
|
71
|
+
* - "llm": Always use LLM agent (requires apiEndpoint)
|
|
72
|
+
* - "rule-based": Always use rule-based agent
|
|
73
|
+
* - "auto": Use LLM if apiEndpoint is provided, fallback to rule-based on error or when offline
|
|
74
|
+
* @default "auto"
|
|
75
|
+
*/
|
|
76
|
+
mode?: AgentMode;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Force rule-based mode (skip LLM even if apiEndpoint is available)
|
|
80
|
+
* @default false
|
|
81
|
+
*/
|
|
82
|
+
forceRuleBased?: boolean;
|
|
83
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Ai11yContext, Ai11yError } from "./context.js";
|
|
2
|
+
import { getContext } from "./dom.js";
|
|
3
|
+
import {
|
|
4
|
+
clickMarker,
|
|
5
|
+
fillInputMarker,
|
|
6
|
+
highlightMarker,
|
|
7
|
+
navigateToRoute,
|
|
8
|
+
scrollToMarker,
|
|
9
|
+
} from "./dom-actions/index.js";
|
|
10
|
+
import type { Instruction } from "./instruction.js";
|
|
11
|
+
import { setError, track as trackToStore } from "./store.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Client interface for interacting with the AI accessibility system
|
|
15
|
+
*/
|
|
16
|
+
export interface Ai11yClient {
|
|
17
|
+
/**
|
|
18
|
+
* Describes the current UI context (markers, route, state, errors)
|
|
19
|
+
*/
|
|
20
|
+
describe(): Ai11yContext;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Executes an instruction on the UI
|
|
24
|
+
*/
|
|
25
|
+
act(instruction: Instruction): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tracks a custom event
|
|
29
|
+
*/
|
|
30
|
+
track(event: string, payload?: unknown): void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reports an error to the system
|
|
34
|
+
*/
|
|
35
|
+
reportError(
|
|
36
|
+
error: Error,
|
|
37
|
+
meta?: { surface?: string; markerId?: string },
|
|
38
|
+
): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface CreateClientOptions {
|
|
42
|
+
onNavigate?: (route: string) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new AI accessibility client instance
|
|
47
|
+
*
|
|
48
|
+
* @param options - Optional configuration
|
|
49
|
+
* @returns An Ai11yClient instance
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const ctx = createClient({ onNavigate: (route) => navigate(route) })
|
|
54
|
+
* const ui = ctx.describe()
|
|
55
|
+
* ctx.act({ action: "click", id: "save_button" })
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function createClient(options?: CreateClientOptions): Ai11yClient {
|
|
59
|
+
const { onNavigate } = options || {};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
describe(): Ai11yContext {
|
|
63
|
+
return getContext();
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
act(instruction: Instruction): void {
|
|
67
|
+
switch (instruction.action) {
|
|
68
|
+
case "click":
|
|
69
|
+
clickMarker(instruction.id);
|
|
70
|
+
break;
|
|
71
|
+
case "navigate":
|
|
72
|
+
if (onNavigate) {
|
|
73
|
+
onNavigate(instruction.route);
|
|
74
|
+
} else {
|
|
75
|
+
navigateToRoute(instruction.route);
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case "highlight":
|
|
79
|
+
highlightMarker(instruction.id);
|
|
80
|
+
break;
|
|
81
|
+
case "scroll":
|
|
82
|
+
scrollToMarker(instruction.id);
|
|
83
|
+
break;
|
|
84
|
+
case "fillInput":
|
|
85
|
+
fillInputMarker(instruction.id, instruction.value);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
track(event: string, payload?: unknown): void {
|
|
91
|
+
trackToStore(event, payload);
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
reportError(
|
|
95
|
+
error: Error,
|
|
96
|
+
meta?: { surface?: string; markerId?: string },
|
|
97
|
+
): void {
|
|
98
|
+
const assistError: Ai11yError = {
|
|
99
|
+
error,
|
|
100
|
+
meta,
|
|
101
|
+
timestamp: Date.now(),
|
|
102
|
+
};
|
|
103
|
+
setError(assistError);
|
|
104
|
+
trackToStore("error", { error: error.message, meta });
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Marker } from "./marker.js";
|
|
2
|
+
|
|
3
|
+
export interface Ai11yState {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Ai11yError {
|
|
8
|
+
error: Error;
|
|
9
|
+
meta?: {
|
|
10
|
+
surface?: string;
|
|
11
|
+
markerId?: string;
|
|
12
|
+
};
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Ai11yEvent {
|
|
17
|
+
type: string;
|
|
18
|
+
payload?: unknown;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Ai11yContext {
|
|
23
|
+
markers: Marker[];
|
|
24
|
+
inViewMarkerIds?: string[];
|
|
25
|
+
route?: string;
|
|
26
|
+
state?: Ai11yState;
|
|
27
|
+
error?: Ai11yError | null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { track } from "../store.js";
|
|
2
|
+
import { findMarkerElement } from "./find-element.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Clicks a marker element by its ID
|
|
6
|
+
*
|
|
7
|
+
* @param markerId - The marker ID to click
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* clickMarker('connect_stripe');
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function clickMarker(markerId: string): void {
|
|
15
|
+
const element = findMarkerElement(markerId);
|
|
16
|
+
if (!element) {
|
|
17
|
+
console.warn(`Marker ${markerId} not found`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Prefer native click to avoid double-firing handlers (important for toggles)
|
|
22
|
+
if (
|
|
23
|
+
"click" in element &&
|
|
24
|
+
typeof (element as HTMLElement).click === "function"
|
|
25
|
+
) {
|
|
26
|
+
(element as HTMLElement).click();
|
|
27
|
+
} else {
|
|
28
|
+
// Fallback: dispatch a synthetic mouse event
|
|
29
|
+
const mouseEvent = new MouseEvent("click", {
|
|
30
|
+
view: window,
|
|
31
|
+
bubbles: true,
|
|
32
|
+
cancelable: true,
|
|
33
|
+
buttons: 1,
|
|
34
|
+
});
|
|
35
|
+
element.dispatchEvent(mouseEvent);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
track("click", { markerId });
|
|
39
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { track } from "../store.js";
|
|
2
|
+
import { findMarkerElement } from "./find-element.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fills an input, textarea, or select element by its marker ID with a value, emitting native browser events
|
|
6
|
+
*
|
|
7
|
+
* @param markerId - The marker ID of the input/textarea/select element to fill
|
|
8
|
+
* @param value - The value to fill into the element. For select elements, this must match one of the available option values.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* fillInputMarker('email_input', 'test@example.com');
|
|
13
|
+
* fillInputMarker('category_select', 'feedback');
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function fillInputMarker(markerId: string, value: string): void {
|
|
17
|
+
const element = findMarkerElement(markerId);
|
|
18
|
+
if (!element) {
|
|
19
|
+
console.warn(`Marker ${markerId} not found`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If the marked element is not itself an input, try to find an input inside it
|
|
24
|
+
// (in case Mark wraps the input in a container)
|
|
25
|
+
let inputElement: HTMLInputElement | HTMLTextAreaElement | null = null;
|
|
26
|
+
let selectElement: HTMLSelectElement | null = null;
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
element instanceof HTMLInputElement ||
|
|
30
|
+
element instanceof HTMLTextAreaElement
|
|
31
|
+
) {
|
|
32
|
+
inputElement = element;
|
|
33
|
+
} else if (element instanceof HTMLSelectElement) {
|
|
34
|
+
selectElement = element;
|
|
35
|
+
} else if (element instanceof HTMLElement) {
|
|
36
|
+
// Search for input, textarea, or select within the marked element
|
|
37
|
+
const nestedInput = element.querySelector("input, textarea");
|
|
38
|
+
if (
|
|
39
|
+
nestedInput instanceof HTMLInputElement ||
|
|
40
|
+
nestedInput instanceof HTMLTextAreaElement
|
|
41
|
+
) {
|
|
42
|
+
inputElement = nestedInput;
|
|
43
|
+
} else {
|
|
44
|
+
const nestedSelect = element.querySelector("select");
|
|
45
|
+
if (nestedSelect instanceof HTMLSelectElement) {
|
|
46
|
+
selectElement = nestedSelect;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isContentEditable =
|
|
52
|
+
element instanceof HTMLElement && element.contentEditable === "true";
|
|
53
|
+
|
|
54
|
+
if (!inputElement && !selectElement && !isContentEditable) {
|
|
55
|
+
console.warn(
|
|
56
|
+
`Marker ${markerId} does not contain an input, textarea, select, or contenteditable element`,
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Set the value property (for React controlled components)
|
|
62
|
+
if (inputElement) {
|
|
63
|
+
// Step 1: Directly set the value
|
|
64
|
+
// For React-controlled inputs, we must use the native setter to bypass React's value prop
|
|
65
|
+
const prototype =
|
|
66
|
+
inputElement instanceof HTMLTextAreaElement
|
|
67
|
+
? HTMLTextAreaElement.prototype
|
|
68
|
+
: HTMLInputElement.prototype;
|
|
69
|
+
const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
|
|
70
|
+
|
|
71
|
+
if (setter) {
|
|
72
|
+
// Use native setter for React-controlled inputs
|
|
73
|
+
setter.call(inputElement, value);
|
|
74
|
+
} else {
|
|
75
|
+
// Fallback: direct assignment for native inputs
|
|
76
|
+
inputElement.value = value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Step 2: Dispatch the right events with bubbles: true
|
|
80
|
+
// This is crucial - bubbles: true allows React/Vue/Angular to catch the events
|
|
81
|
+
// input event → updates live state
|
|
82
|
+
inputElement.dispatchEvent(new Event("input", { bubbles: true }));
|
|
83
|
+
|
|
84
|
+
// change event → commits value (important for forms)
|
|
85
|
+
inputElement.dispatchEvent(new Event("change", { bubbles: true }));
|
|
86
|
+
|
|
87
|
+
// Step 3: Focus management (optional but useful)
|
|
88
|
+
// Helps with validation, conditional UI, and accessibility expectations
|
|
89
|
+
inputElement.focus();
|
|
90
|
+
} else if (selectElement) {
|
|
91
|
+
// For select elements, set the value property
|
|
92
|
+
selectElement.value = value;
|
|
93
|
+
|
|
94
|
+
// Dispatch change event to trigger React onChange handlers
|
|
95
|
+
selectElement.dispatchEvent(new Event("change", { bubbles: true }));
|
|
96
|
+
|
|
97
|
+
// Focus the select element
|
|
98
|
+
selectElement.focus();
|
|
99
|
+
} else if (isContentEditable) {
|
|
100
|
+
// For contenteditable elements, set textContent and dispatch events
|
|
101
|
+
const editableElement = element as HTMLElement;
|
|
102
|
+
editableElement.textContent = value;
|
|
103
|
+
|
|
104
|
+
// Dispatch input event
|
|
105
|
+
const inputEvent = new InputEvent("input", {
|
|
106
|
+
bubbles: true,
|
|
107
|
+
cancelable: true,
|
|
108
|
+
});
|
|
109
|
+
element.dispatchEvent(inputEvent);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
track("fillInput", { markerId, value });
|
|
113
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getMarkerSelector } from "../util/attributes.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Finds an element by its marker ID (data-ai-id attribute)
|
|
5
|
+
*
|
|
6
|
+
* @param markerId - The marker ID to find
|
|
7
|
+
* @returns The element if found, null otherwise
|
|
8
|
+
*/
|
|
9
|
+
export function findMarkerElement(markerId: string): Element | null {
|
|
10
|
+
if (typeof document === "undefined") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return document.querySelector(getMarkerSelector(markerId));
|
|
14
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { track } from "../store.js";
|
|
2
|
+
import { findMarkerElement } from "./find-element.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for highlighting a marker
|
|
6
|
+
*/
|
|
7
|
+
export interface HighlightOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Callback function called when marker is highlighted
|
|
10
|
+
*/
|
|
11
|
+
onHighlight?: (markerId: string, element: Element) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Duration in milliseconds for the highlight (default: 2000)
|
|
14
|
+
* Set to 0 to skip visual highlighting (useful when using custom highlight wrapper)
|
|
15
|
+
*/
|
|
16
|
+
duration?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if an element is currently visible in the viewport
|
|
21
|
+
*/
|
|
22
|
+
function isElementInView(element: Element): boolean {
|
|
23
|
+
const rect = element.getBoundingClientRect();
|
|
24
|
+
const windowHeight =
|
|
25
|
+
window.innerHeight || document.documentElement.clientHeight;
|
|
26
|
+
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
27
|
+
|
|
28
|
+
const isPartiallyVisible =
|
|
29
|
+
rect.top < windowHeight &&
|
|
30
|
+
rect.bottom > 0 &&
|
|
31
|
+
rect.left < windowWidth &&
|
|
32
|
+
rect.right > 0;
|
|
33
|
+
|
|
34
|
+
return isPartiallyVisible;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Highlights a marker element by its ID
|
|
39
|
+
* Scrolls the element into view (only if not already in view) and applies visual highlighting
|
|
40
|
+
*
|
|
41
|
+
* @param markerId - The marker ID to highlight
|
|
42
|
+
* @param options - Optional configuration for highlighting
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* highlightMarker('connect_stripe', {
|
|
47
|
+
* onHighlight: (id, el) => console.log('Highlighted:', id),
|
|
48
|
+
* duration: 3000
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function highlightMarker(
|
|
53
|
+
markerId: string,
|
|
54
|
+
options: HighlightOptions = {},
|
|
55
|
+
): void {
|
|
56
|
+
const element = findMarkerElement(markerId);
|
|
57
|
+
if (!element) {
|
|
58
|
+
console.warn(`Marker ${markerId} not found`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { onHighlight, duration = 2000 } = options;
|
|
63
|
+
|
|
64
|
+
if (!isElementInView(element)) {
|
|
65
|
+
element.scrollIntoView({
|
|
66
|
+
behavior: "smooth",
|
|
67
|
+
block: "center",
|
|
68
|
+
inline: "nearest",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (onHighlight) {
|
|
73
|
+
onHighlight(markerId, element);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (duration > 0) {
|
|
77
|
+
const originalOutline = (element as HTMLElement).style.outline;
|
|
78
|
+
const originalOutlineOffset = (element as HTMLElement).style.outlineOffset;
|
|
79
|
+
const originalTransition = (element as HTMLElement).style.transition;
|
|
80
|
+
|
|
81
|
+
(element as HTMLElement).style.outline = "3px solid #3b82f6";
|
|
82
|
+
(element as HTMLElement).style.outlineOffset = "2px";
|
|
83
|
+
(element as HTMLElement).style.transition = "outline 0.2s ease";
|
|
84
|
+
|
|
85
|
+
window.setTimeout(() => {
|
|
86
|
+
(element as HTMLElement).style.outline = originalOutline;
|
|
87
|
+
(element as HTMLElement).style.outlineOffset = originalOutlineOffset;
|
|
88
|
+
(element as HTMLElement).style.transition = originalTransition;
|
|
89
|
+
}, duration);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
track("highlight", { markerId });
|
|
93
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { setRoute, track } from "../store.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Navigates to a route
|
|
5
|
+
* Updates the route in the store and tracks the navigation event
|
|
6
|
+
*
|
|
7
|
+
* @param route - The route to navigate to
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* navigateToRoute('/billing');
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function navigateToRoute(route: string): void {
|
|
15
|
+
setRoute(route);
|
|
16
|
+
track("navigate", { route });
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { track } from "../store.js";
|
|
2
|
+
import { findMarkerElement } from "./find-element.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scrolls to a marker element by its ID
|
|
6
|
+
* Does not apply highlight animation - use highlightMarker() if you want both scroll and highlight
|
|
7
|
+
*
|
|
8
|
+
* @param markerId - The marker ID to scroll to
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* scrollToMarker('connect_stripe');
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function scrollToMarker(markerId: string): void {
|
|
16
|
+
const element = findMarkerElement(markerId);
|
|
17
|
+
if (!element) {
|
|
18
|
+
console.warn(`Marker ${markerId} not found`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
element.scrollIntoView({
|
|
23
|
+
behavior: "smooth",
|
|
24
|
+
block: "center",
|
|
25
|
+
inline: "nearest",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
track("scroll", { markerId });
|
|
29
|
+
}
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Ai11yContext } from "./context.js";
|
|
2
|
+
import { getMarkers } from "./marker.js";
|
|
3
|
+
import { getError, getRoute, getState } from "./store.js";
|
|
4
|
+
import { getAllMarkersSelector, getMarkerId } from "./util/attributes.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detects which markers are currently visible in the viewport
|
|
8
|
+
*
|
|
9
|
+
* @param root - Optional DOM root element to scan for markers (defaults to document.body)
|
|
10
|
+
* @returns Array of marker IDs that are currently in view
|
|
11
|
+
*/
|
|
12
|
+
function getInViewMarkerIds(root?: Element): string[] {
|
|
13
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const scanRoot = root ?? document.body;
|
|
18
|
+
if (!scanRoot) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const elements = scanRoot.querySelectorAll(getAllMarkersSelector());
|
|
23
|
+
const inViewIds: string[] = [];
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < elements.length; i++) {
|
|
26
|
+
const element = elements[i];
|
|
27
|
+
const id = getMarkerId(element);
|
|
28
|
+
if (!id) continue;
|
|
29
|
+
|
|
30
|
+
const rect = element.getBoundingClientRect();
|
|
31
|
+
const isInView =
|
|
32
|
+
rect.top >= 0 &&
|
|
33
|
+
rect.left >= 0 &&
|
|
34
|
+
rect.bottom <=
|
|
35
|
+
(window.innerHeight || document.documentElement.clientHeight) &&
|
|
36
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
|
37
|
+
|
|
38
|
+
const isPartiallyVisible =
|
|
39
|
+
rect.top <
|
|
40
|
+
(window.innerHeight || document.documentElement.clientHeight) &&
|
|
41
|
+
rect.bottom > 0 &&
|
|
42
|
+
rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
|
|
43
|
+
rect.right > 0;
|
|
44
|
+
|
|
45
|
+
if (isInView || isPartiallyVisible) {
|
|
46
|
+
inViewIds.push(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return inViewIds;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Composes a complete Ai11yContext from singleton state and DOM markers
|
|
55
|
+
*
|
|
56
|
+
* @param root - Optional DOM root element to scan for markers (defaults to document.body)
|
|
57
|
+
* @returns A complete Ai11yContext object with markers from DOM and state from singleton
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { setRoute, setState, getContext } from '@ai11y/core';
|
|
62
|
+
*
|
|
63
|
+
* setRoute('/billing');
|
|
64
|
+
* setState({ userId: '123' });
|
|
65
|
+
* const context = getContext();
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function getContext(root?: Element): Ai11yContext {
|
|
69
|
+
const context: Ai11yContext = {
|
|
70
|
+
markers: getMarkers(root),
|
|
71
|
+
inViewMarkerIds: getInViewMarkerIds(root),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const route = getRoute();
|
|
75
|
+
const state = getState();
|
|
76
|
+
const error = getError();
|
|
77
|
+
|
|
78
|
+
if (route !== undefined) {
|
|
79
|
+
context.route = route;
|
|
80
|
+
}
|
|
81
|
+
if (state !== undefined) {
|
|
82
|
+
context.state = state;
|
|
83
|
+
}
|
|
84
|
+
if (error !== undefined) {
|
|
85
|
+
context.error = error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return context;
|
|
89
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event listener function type
|
|
3
|
+
*/
|
|
4
|
+
type EventListener = () => void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Set of event listeners
|
|
8
|
+
*/
|
|
9
|
+
const listeners = new Set<EventListener>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to event notifications
|
|
13
|
+
* Returns an unsubscribe function
|
|
14
|
+
*
|
|
15
|
+
* @param listener - Function to call when events are tracked
|
|
16
|
+
* @returns Unsubscribe function
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const unsubscribe = subscribe(() => {
|
|
21
|
+
* console.log('Event tracked!');
|
|
22
|
+
* });
|
|
23
|
+
* // Later...
|
|
24
|
+
* unsubscribe();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function subscribe(listener: EventListener): () => void {
|
|
28
|
+
listeners.add(listener);
|
|
29
|
+
return () => {
|
|
30
|
+
listeners.delete(listener);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Notify all subscribers that an event has been tracked
|
|
36
|
+
* This is called internally by the store when track() is called
|
|
37
|
+
*/
|
|
38
|
+
export function notify(): void {
|
|
39
|
+
for (const listener of listeners) {
|
|
40
|
+
try {
|
|
41
|
+
listener();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// Don't let one listener's error break others
|
|
44
|
+
console.error("Error in event listener:", error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the number of active subscribers
|
|
51
|
+
* Useful for debugging
|
|
52
|
+
*/
|
|
53
|
+
export function getSubscriberCount(): number {
|
|
54
|
+
return listeners.size;
|
|
55
|
+
}
|