@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.
Files changed (107) hide show
  1. package/.turbo/turbo-build.log +87 -0
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +37 -0
  4. package/dist/agent/agent-adapter.d.mts +17 -0
  5. package/dist/agent/agent-adapter.d.mts.map +1 -0
  6. package/dist/agent/agent-adapter.mjs +45 -0
  7. package/dist/agent/agent-adapter.mjs.map +1 -0
  8. package/dist/agent/llm-agent.d.mts +13 -0
  9. package/dist/agent/llm-agent.d.mts.map +1 -0
  10. package/dist/agent/llm-agent.mjs +41 -0
  11. package/dist/agent/llm-agent.mjs.map +1 -0
  12. package/dist/agent/plan.d.mts +27 -0
  13. package/dist/agent/plan.d.mts.map +1 -0
  14. package/dist/agent/plan.mjs +28 -0
  15. package/dist/agent/plan.mjs.map +1 -0
  16. package/dist/agent/rule-based-agent.d.mts +13 -0
  17. package/dist/agent/rule-based-agent.d.mts.map +1 -0
  18. package/dist/agent/rule-based-agent.mjs +152 -0
  19. package/dist/agent/rule-based-agent.mjs.map +1 -0
  20. package/dist/agent/tool-contract.d.mts +19 -0
  21. package/dist/agent/tool-contract.d.mts.map +1 -0
  22. package/dist/agent/types.d.mts +77 -0
  23. package/dist/agent/types.d.mts.map +1 -0
  24. package/dist/client-api.d.mts +49 -0
  25. package/dist/client-api.d.mts.map +1 -0
  26. package/dist/client-api.mjs +68 -0
  27. package/dist/client-api.mjs.map +1 -0
  28. package/dist/context.d.mts +29 -0
  29. package/dist/context.d.mts.map +1 -0
  30. package/dist/dom-actions/click.d.mts +15 -0
  31. package/dist/dom-actions/click.d.mts.map +1 -0
  32. package/dist/dom-actions/click.mjs +36 -0
  33. package/dist/dom-actions/click.mjs.map +1 -0
  34. package/dist/dom-actions/fill-input.d.mts +17 -0
  35. package/dist/dom-actions/fill-input.d.mts.map +1 -0
  36. package/dist/dom-actions/fill-input.mjs +69 -0
  37. package/dist/dom-actions/fill-input.mjs.map +1 -0
  38. package/dist/dom-actions/find-element.mjs +17 -0
  39. package/dist/dom-actions/find-element.mjs.map +1 -0
  40. package/dist/dom-actions/highlight.d.mts +34 -0
  41. package/dist/dom-actions/highlight.d.mts.map +1 -0
  42. package/dist/dom-actions/highlight.mjs +60 -0
  43. package/dist/dom-actions/highlight.mjs.map +1 -0
  44. package/dist/dom-actions/navigate.d.mts +16 -0
  45. package/dist/dom-actions/navigate.d.mts.map +1 -0
  46. package/dist/dom-actions/navigate.mjs +22 -0
  47. package/dist/dom-actions/navigate.mjs.map +1 -0
  48. package/dist/dom-actions/scroll.d.mts +16 -0
  49. package/dist/dom-actions/scroll.d.mts.map +1 -0
  50. package/dist/dom-actions/scroll.mjs +32 -0
  51. package/dist/dom-actions/scroll.mjs.map +1 -0
  52. package/dist/dom.d.mts +23 -0
  53. package/dist/dom.d.mts.map +1 -0
  54. package/dist/dom.mjs +60 -0
  55. package/dist/dom.mjs.map +1 -0
  56. package/dist/events.d.mts +35 -0
  57. package/dist/events.d.mts.map +1 -0
  58. package/dist/events.mjs +49 -0
  59. package/dist/events.mjs.map +1 -0
  60. package/dist/index.d.mts +21 -0
  61. package/dist/index.mjs +18 -0
  62. package/dist/instruction.d.mts +21 -0
  63. package/dist/instruction.d.mts.map +1 -0
  64. package/dist/marker.d.mts +35 -0
  65. package/dist/marker.d.mts.map +1 -0
  66. package/dist/marker.mjs +137 -0
  67. package/dist/marker.mjs.map +1 -0
  68. package/dist/store.d.mts +56 -0
  69. package/dist/store.d.mts.map +1 -0
  70. package/dist/store.mjs +114 -0
  71. package/dist/store.mjs.map +1 -0
  72. package/dist/util/attributes.d.mts +57 -0
  73. package/dist/util/attributes.d.mts.map +1 -0
  74. package/dist/util/attributes.mjs +68 -0
  75. package/dist/util/attributes.mjs.map +1 -0
  76. package/dist/util/format.d.mts +18 -0
  77. package/dist/util/format.d.mts.map +1 -0
  78. package/dist/util/format.mjs +21 -0
  79. package/dist/util/format.mjs.map +1 -0
  80. package/package.json +26 -0
  81. package/src/agent/agent-adapter.ts +75 -0
  82. package/src/agent/index.ts +21 -0
  83. package/src/agent/llm-agent.ts +64 -0
  84. package/src/agent/plan.ts +41 -0
  85. package/src/agent/rule-based-agent.ts +269 -0
  86. package/src/agent/tool-contract.ts +22 -0
  87. package/src/agent/types.ts +83 -0
  88. package/src/client-api.ts +107 -0
  89. package/src/context.ts +28 -0
  90. package/src/dom-actions/click.ts +39 -0
  91. package/src/dom-actions/fill-input.ts +113 -0
  92. package/src/dom-actions/find-element.ts +14 -0
  93. package/src/dom-actions/highlight.ts +93 -0
  94. package/src/dom-actions/index.ts +5 -0
  95. package/src/dom-actions/navigate.ts +17 -0
  96. package/src/dom-actions/scroll.ts +29 -0
  97. package/src/dom.ts +89 -0
  98. package/src/events.ts +55 -0
  99. package/src/index.ts +55 -0
  100. package/src/instruction.ts +6 -0
  101. package/src/marker.ts +237 -0
  102. package/src/store.ts +138 -0
  103. package/src/util/attributes.ts +68 -0
  104. package/src/util/format.ts +16 -0
  105. package/src/util/index.ts +2 -0
  106. package/tsconfig.json +18 -0
  107. 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,5 @@
1
+ export { clickMarker } from "./click.js";
2
+ export { fillInputMarker } from "./fill-input.js";
3
+ export { type HighlightOptions, highlightMarker } from "./highlight.js";
4
+ export { navigateToRoute } from "./navigate.js";
5
+ export { scrollToMarker } from "./scroll.js";
@@ -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
+ }