begeniux 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,52 +1,79 @@
1
1
  // src/BeGenProvider.tsx
2
2
  import * as React from "react";
3
- import { jsx } from "react/jsx-runtime";
4
- var BeGenContext = React.createContext(null);
5
- function BeGenProvider({ children }) {
6
- const [variant, setVariant] = React.useState("neutral");
7
- const [directive, setDirective] = React.useState(null);
8
- const [summary, setSummary] = React.useState(null);
9
- const value = React.useMemo(
10
- () => ({ variant, directive, summary, setVariant, setDirective, setSummary }),
11
- [variant, directive, summary]
12
- );
13
- return /* @__PURE__ */ jsx(BeGenContext.Provider, { value, children });
14
- }
15
-
16
- // src/BeGenSurface.tsx
17
- import * as React2 from "react";
18
3
 
19
4
  // src/useBehaviorTracker.ts
20
- import { useEffect, useRef, useState as useState2 } from "react";
21
- var RECENT_EVENTS_CAP = 5;
5
+ import { useEffect, useRef, useState } from "react";
6
+ var RECENT_EVENTS_CAP = 10;
22
7
  var HOVER_MIN_MS = 200;
8
+ var RAGE_CLICK_WINDOW_MS = 1e3;
9
+ var RAGE_CLICK_THRESHOLD = 3;
10
+ var INPUT_THROTTLE_MS = 1e3;
23
11
  function targetLabel(el) {
24
12
  if (!(el instanceof HTMLElement)) return "unknown";
25
- return el.dataset.begenId || el.getAttribute("data-product-id") || el.getAttribute("aria-label") || el.tagName.toLowerCase();
13
+ const begenId = el.getAttribute("data-begen-id");
14
+ if (begenId) return `[data-begen-id="${begenId}"]`;
15
+ if (el.id) return `#${el.id}`;
16
+ const testId = el.getAttribute("data-testid");
17
+ if (testId) return `[data-testid="${testId}"]`;
18
+ return el.tagName.toLowerCase();
26
19
  }
27
- function computeSummary(buffer, bufferSize, pageContext) {
20
+ function computeSummary(buffer, bufferSize, pageContext, custom) {
28
21
  const now = Date.now();
29
22
  const windowStart = now - 6e4;
30
23
  let clicksLastMin = 0;
24
+ let rageClicks = 0;
31
25
  let dwellSum = 0;
32
26
  let dwellCount = 0;
33
27
  let maxScroll = 0;
28
+ let formInteractions = 0;
29
+ let errorsSeen = 0;
34
30
  const hoverTargets = /* @__PURE__ */ new Set();
31
+ let viewportW = typeof window !== "undefined" ? window.innerWidth || 0 : 0;
32
+ let viewportH = typeof window !== "undefined" ? window.innerHeight || 0 : 0;
35
33
  for (const ev of buffer) {
36
- if (ev.kind === "click" && ev.t >= windowStart) clicksLastMin += 1;
37
- if (ev.kind === "dwell") {
38
- dwellSum += ev.durationMs;
39
- dwellCount += 1;
34
+ switch (ev.kind) {
35
+ case "click":
36
+ if (ev.t >= windowStart) clicksLastMin += 1;
37
+ break;
38
+ case "rage-click":
39
+ rageClicks += 1;
40
+ break;
41
+ case "dwell":
42
+ dwellSum += ev.durationMs;
43
+ dwellCount += 1;
44
+ break;
45
+ case "scroll":
46
+ if (ev.depth > maxScroll) maxScroll = ev.depth;
47
+ break;
48
+ case "hover":
49
+ hoverTargets.add(ev.target);
50
+ break;
51
+ case "input":
52
+ case "submit":
53
+ formInteractions += 1;
54
+ break;
55
+ case "error":
56
+ errorsSeen += 1;
57
+ break;
58
+ case "viewport-change":
59
+ viewportW = ev.width;
60
+ viewportH = ev.height;
61
+ break;
62
+ default:
63
+ break;
40
64
  }
41
- if (ev.kind === "scroll" && ev.depth > maxScroll) maxScroll = ev.depth;
42
- if (ev.kind === "hover") hoverTargets.add(ev.target);
43
65
  }
44
66
  return {
45
67
  clicks_per_min: clicksLastMin,
68
+ rage_clicks: rageClicks,
46
69
  avg_dwell_ms: dwellCount === 0 ? 0 : Math.round(dwellSum / dwellCount),
47
70
  scroll_depth: Math.max(0, Math.min(1, maxScroll)),
48
71
  hover_count: hoverTargets.size,
72
+ form_interactions: formInteractions,
73
+ errors_seen: errorsSeen,
49
74
  events_seen: Math.min(buffer.length, bufferSize),
75
+ viewport: { width: viewportW, height: viewportH },
76
+ custom,
50
77
  page_context: pageContext
51
78
  };
52
79
  }
@@ -58,13 +85,19 @@ function useBehaviorTracker(opts) {
58
85
  bufferSize = 50,
59
86
  onFlush,
60
87
  pageContext,
61
- seedTrace
88
+ seedTrace,
89
+ customListeners
62
90
  } = opts;
63
91
  const bufferRef = useRef([]);
64
92
  const sinceFlushRef = useRef(0);
65
93
  const lastFlushAtRef = useRef(Date.now());
66
94
  const hoverStartRef = useRef(/* @__PURE__ */ new Map());
95
+ const focusStartRef = useRef(/* @__PURE__ */ new Map());
96
+ const lastInputAtRef = useRef(/* @__PURE__ */ new Map());
97
+ const recentClicksRef = useRef(/* @__PURE__ */ new Map());
67
98
  const scrollRafRef = useRef(null);
99
+ const resizeRafRef = useRef(null);
100
+ const customSlotRef = useRef({});
68
101
  const onFlushRef = useRef(onFlush);
69
102
  const pageContextRef = useRef(pageContext);
70
103
  useEffect(() => {
@@ -73,7 +106,7 @@ function useBehaviorTracker(opts) {
73
106
  useEffect(() => {
74
107
  pageContextRef.current = pageContext;
75
108
  }, [pageContext]);
76
- const [recentEvents, setRecentEvents] = useState2([]);
109
+ const [recentEvents, setRecentEvents] = useState([]);
77
110
  const pushEvent = (ev) => {
78
111
  const buf = bufferRef.current;
79
112
  buf.push(ev);
@@ -81,7 +114,9 @@ function useBehaviorTracker(opts) {
81
114
  sinceFlushRef.current += 1;
82
115
  setRecentEvents((prev) => {
83
116
  const next = [...prev, ev];
84
- if (next.length > RECENT_EVENTS_CAP) next.splice(0, next.length - RECENT_EVENTS_CAP);
117
+ if (next.length > RECENT_EVENTS_CAP) {
118
+ next.splice(0, next.length - RECENT_EVENTS_CAP);
119
+ }
85
120
  return next;
86
121
  });
87
122
  if (sinceFlushRef.current >= flushEveryEvents) {
@@ -89,31 +124,58 @@ function useBehaviorTracker(opts) {
89
124
  }
90
125
  };
91
126
  const flush = () => {
92
- const summary = computeSummary(bufferRef.current, bufferSize, pageContextRef.current);
127
+ const summary = computeSummary(
128
+ bufferRef.current,
129
+ bufferSize,
130
+ pageContextRef.current,
131
+ customSlotRef.current
132
+ );
93
133
  sinceFlushRef.current = 0;
94
134
  lastFlushAtRef.current = Date.now();
95
135
  onFlushRef.current(summary);
96
136
  };
97
137
  useEffect(() => {
98
- if (seedTrace && seedTrace.length > 0) {
99
- const buf = bufferRef.current;
100
- const maxT = seedTrace.reduce((m, e) => e.t > m ? e.t : m, -Infinity);
101
- const now = Date.now();
102
- const offset = Number.isFinite(maxT) ? now - maxT : 0;
103
- const rebased = seedTrace.map((e) => ({ ...e, t: e.t + offset }));
104
- buf.push(...rebased);
105
- if (buf.length > bufferSize) buf.splice(0, buf.length - bufferSize);
106
- const summary = computeSummary(buf, bufferSize, pageContextRef.current);
107
- sinceFlushRef.current = 0;
108
- lastFlushAtRef.current = Date.now();
109
- onFlushRef.current(summary);
110
- }
138
+ if (!seedTrace || seedTrace.length === 0) return;
139
+ const buf = bufferRef.current;
140
+ const maxT = seedTrace.reduce((m, e) => e.t > m ? e.t : m, -Infinity);
141
+ const now = Date.now();
142
+ const offset = Number.isFinite(maxT) ? now - maxT : 0;
143
+ const rebased = seedTrace.map((e) => ({ ...e, t: e.t + offset }));
144
+ buf.push(...rebased);
145
+ if (buf.length > bufferSize) buf.splice(0, buf.length - bufferSize);
146
+ const summary = computeSummary(
147
+ buf,
148
+ bufferSize,
149
+ pageContextRef.current,
150
+ customSlotRef.current
151
+ );
152
+ sinceFlushRef.current = 0;
153
+ lastFlushAtRef.current = Date.now();
154
+ onFlushRef.current(summary);
111
155
  }, []);
112
156
  useEffect(() => {
113
- const el = containerRef.current;
157
+ if (typeof window === "undefined" || typeof document === "undefined") return;
158
+ const el = containerRef?.current ?? (typeof document !== "undefined" ? document.body : null);
114
159
  if (!el) return;
115
160
  const onClick = (e) => {
116
- pushEvent({ kind: "click", target: targetLabel(e.target), t: Date.now() });
161
+ const label = targetLabel(e.target);
162
+ const t = Date.now();
163
+ pushEvent({ kind: "click", target: label, t });
164
+ const recent = recentClicksRef.current.get(label) ?? [];
165
+ const filtered = recent.filter(
166
+ (then) => t - then < RAGE_CLICK_WINDOW_MS
167
+ );
168
+ filtered.push(t);
169
+ recentClicksRef.current.set(label, filtered);
170
+ if (filtered.length >= RAGE_CLICK_THRESHOLD) {
171
+ pushEvent({
172
+ kind: "rage-click",
173
+ target: label,
174
+ count: filtered.length,
175
+ t
176
+ });
177
+ recentClicksRef.current.set(label, []);
178
+ }
117
179
  };
118
180
  const onMouseOver = (e) => {
119
181
  const label = targetLabel(e.target);
@@ -132,11 +194,46 @@ function useBehaviorTracker(opts) {
132
194
  pushEvent({ kind: "hover", target: label, durationMs, t });
133
195
  pushEvent({ kind: "dwell", target: label, durationMs, t });
134
196
  };
197
+ const onFocusIn = (e) => {
198
+ const label = targetLabel(e.target);
199
+ focusStartRef.current.set(label, Date.now());
200
+ pushEvent({ kind: "focus", target: label, t: Date.now() });
201
+ };
202
+ const onFocusOut = (e) => {
203
+ const label = targetLabel(e.target);
204
+ const started = focusStartRef.current.get(label);
205
+ if (started == null) return;
206
+ focusStartRef.current.delete(label);
207
+ const durationMs = Date.now() - started;
208
+ if (durationMs < HOVER_MIN_MS) return;
209
+ pushEvent({
210
+ kind: "blur",
211
+ target: label,
212
+ durationMs,
213
+ t: Date.now()
214
+ });
215
+ };
216
+ const onInput = (e) => {
217
+ const target = e.target;
218
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target instanceof HTMLElement && target.isContentEditable)) {
219
+ return;
220
+ }
221
+ const label = targetLabel(target);
222
+ const now = Date.now();
223
+ const last = lastInputAtRef.current.get(label) ?? 0;
224
+ if (now - last < INPUT_THROTTLE_MS) return;
225
+ lastInputAtRef.current.set(label, now);
226
+ pushEvent({ kind: "input", target: label, t: now });
227
+ };
228
+ const onSubmit = (e) => {
229
+ const label = targetLabel(e.target);
230
+ pushEvent({ kind: "submit", target: label, t: Date.now() });
231
+ };
135
232
  const computeScrollDepth = () => {
136
233
  const rect = el.getBoundingClientRect();
137
- const viewportH = window.innerHeight || 1;
234
+ const viewportHCalc = window.innerHeight || 1;
138
235
  const elementH = el.scrollHeight || rect.height || 1;
139
- const visibleBottom = Math.min(rect.bottom, viewportH);
236
+ const visibleBottom = Math.min(rect.bottom, viewportHCalc);
140
237
  const scrolledPast = Math.max(0, visibleBottom - rect.top);
141
238
  return Math.max(0, Math.min(1, scrolledPast / elementH));
142
239
  };
@@ -144,13 +241,60 @@ function useBehaviorTracker(opts) {
144
241
  if (scrollRafRef.current != null) return;
145
242
  scrollRafRef.current = requestAnimationFrame(() => {
146
243
  scrollRafRef.current = null;
147
- pushEvent({ kind: "scroll", depth: computeScrollDepth(), t: Date.now() });
244
+ pushEvent({
245
+ kind: "scroll",
246
+ depth: computeScrollDepth(),
247
+ t: Date.now()
248
+ });
249
+ });
250
+ };
251
+ const onResize = () => {
252
+ if (resizeRafRef.current != null) return;
253
+ resizeRafRef.current = requestAnimationFrame(() => {
254
+ resizeRafRef.current = null;
255
+ pushEvent({
256
+ kind: "viewport-change",
257
+ width: window.innerWidth,
258
+ height: window.innerHeight,
259
+ t: Date.now()
260
+ });
261
+ });
262
+ };
263
+ const onError = (e) => {
264
+ pushEvent({
265
+ kind: "error",
266
+ message: String(e.message ?? "unknown"),
267
+ t: Date.now()
268
+ });
269
+ };
270
+ const onUnhandledRejection = (e) => {
271
+ pushEvent({
272
+ kind: "error",
273
+ message: `unhandled-rejection: ${String(e.reason ?? "unknown")}`,
274
+ t: Date.now()
148
275
  });
149
276
  };
150
277
  el.addEventListener("click", onClick);
151
278
  el.addEventListener("mouseover", onMouseOver);
152
279
  el.addEventListener("mouseout", onMouseOut);
280
+ el.addEventListener("focusin", onFocusIn);
281
+ el.addEventListener("focusout", onFocusOut);
282
+ el.addEventListener("input", onInput);
283
+ el.addEventListener("submit", onSubmit);
153
284
  window.addEventListener("scroll", onScroll, { passive: true });
285
+ window.addEventListener("resize", onResize);
286
+ window.addEventListener("error", onError);
287
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
288
+ const cleanups = [];
289
+ if (customListeners) {
290
+ for (const listener of customListeners) {
291
+ try {
292
+ const cleanup = listener.attach(el, pushEvent);
293
+ cleanups.push(cleanup);
294
+ } catch {
295
+ }
296
+ }
297
+ }
154
298
  const interval = window.setInterval(() => {
155
299
  if (Date.now() - lastFlushAtRef.current >= flushAfterMs) {
156
300
  flush();
@@ -160,320 +304,513 @@ function useBehaviorTracker(opts) {
160
304
  el.removeEventListener("click", onClick);
161
305
  el.removeEventListener("mouseover", onMouseOver);
162
306
  el.removeEventListener("mouseout", onMouseOut);
307
+ el.removeEventListener("focusin", onFocusIn);
308
+ el.removeEventListener("focusout", onFocusOut);
309
+ el.removeEventListener("input", onInput);
310
+ el.removeEventListener("submit", onSubmit);
163
311
  window.removeEventListener("scroll", onScroll);
312
+ window.removeEventListener("resize", onResize);
313
+ window.removeEventListener("error", onError);
314
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
164
315
  window.clearInterval(interval);
165
- if (scrollRafRef.current != null) cancelAnimationFrame(scrollRafRef.current);
316
+ if (scrollRafRef.current != null) {
317
+ cancelAnimationFrame(scrollRafRef.current);
318
+ }
319
+ if (resizeRafRef.current != null) {
320
+ cancelAnimationFrame(resizeRafRef.current);
321
+ }
322
+ for (const cleanup of cleanups) {
323
+ try {
324
+ cleanup();
325
+ } catch {
326
+ }
327
+ }
166
328
  };
167
329
  }, [containerRef, flushAfterMs, flushEveryEvents, bufferSize]);
168
330
  return { recentEvents };
169
331
  }
170
332
 
171
- // src/personas.ts
172
- function buildDecisive() {
173
- const events = [];
174
- const productIds = ["p-101", "p-102", "p-103", "p-104", "p-105", "p-106"];
175
- let t = 0;
176
- for (let i = 0; i < 12; i++) {
177
- t += 1500;
178
- events.push({ kind: "click", target: productIds[i % productIds.length], t });
333
+ // src/AdaptationEngine.ts
334
+ var STRUCTURAL_PROPERTY_DENY = /* @__PURE__ */ new Set([
335
+ "display",
336
+ "position",
337
+ "visibility",
338
+ "float",
339
+ "clear",
340
+ "z-index",
341
+ "overflow",
342
+ "overflow-x",
343
+ "overflow-y",
344
+ "transform-origin"
345
+ ]);
346
+ var AdaptationEngine = class {
347
+ constructor(opts) {
348
+ this.revertLog = [];
349
+ this.appliedSnapshot = [];
350
+ this.root = opts.root;
351
+ this.scope = opts.scope ?? {};
352
+ this.onEvent = opts.onEvent;
179
353
  }
180
- for (let i = 0; i < 6; i++) {
181
- t += 800;
182
- events.push({
183
- kind: "dwell",
184
- target: productIds[i % productIds.length],
185
- durationMs: 600 + i * 80,
186
- t
187
- });
354
+ /** Apply a plan. Reverts the previous plan first so mutations don't accumulate. */
355
+ apply(plan) {
356
+ this.revertAll();
357
+ for (const adaptation of plan.adaptations) {
358
+ this.applyOne(adaptation);
359
+ }
188
360
  }
189
- for (let i = 0; i < 6; i++) {
190
- t += 1200;
191
- events.push({
192
- kind: "scroll",
193
- depth: Math.min(0.95, 0.4 + i * 0.1),
194
- t
195
- });
361
+ /** Roll back every adaptation applied since the last revertAll. */
362
+ revertAll() {
363
+ if (this.revertLog.length === 0) return;
364
+ const count = this.revertLog.length;
365
+ for (let i = this.revertLog.length - 1; i >= 0; i--) {
366
+ try {
367
+ this.revertLog[i]();
368
+ } catch {
369
+ }
370
+ }
371
+ this.revertLog = [];
372
+ this.appliedSnapshot = [];
373
+ this.onEvent?.({ kind: "reverted", count });
374
+ }
375
+ /** Read-only view of currently applied adaptations (for telemetry). */
376
+ getApplied() {
377
+ return this.appliedSnapshot;
196
378
  }
197
- for (let i = 0; i < 2; i++) {
198
- t += 600;
199
- events.push({
200
- kind: "hover",
201
- target: productIds[i],
202
- durationMs: 250,
203
- t
379
+ // ── Internals ──────────────────────────────────────────────────────
380
+ applyOne(adaptation) {
381
+ const elements = this.resolveTargets(adaptation);
382
+ if (elements === null) return;
383
+ if (elements.length === 0) {
384
+ this.onEvent?.({ kind: "skipped", adaptation, reason: "no-match" });
385
+ return;
386
+ }
387
+ if (adaptation.kind === "set-style" && STRUCTURAL_PROPERTY_DENY.has(adaptation.property.toLowerCase())) {
388
+ this.onEvent?.({
389
+ kind: "skipped",
390
+ adaptation,
391
+ reason: `structural-property-denied:${adaptation.property}`
392
+ });
393
+ return;
394
+ }
395
+ for (const el of elements) {
396
+ const revert = this.applyToElement(el, adaptation);
397
+ if (revert) this.revertLog.push(revert);
398
+ }
399
+ this.appliedSnapshot.push(adaptation);
400
+ this.onEvent?.({
401
+ kind: "applied",
402
+ adaptation,
403
+ matched: elements.length
204
404
  });
205
405
  }
206
- return events;
406
+ resolveTargets(adaptation) {
407
+ const sel = adaptation.selector;
408
+ if (!sel || typeof sel !== "string") {
409
+ this.onEvent?.({
410
+ kind: "skipped",
411
+ adaptation,
412
+ reason: "invalid-selector"
413
+ });
414
+ return null;
415
+ }
416
+ if (this.scope.deny?.some((d) => this.selectorMatchesPattern(sel, d))) {
417
+ this.onEvent?.({ kind: "skipped", adaptation, reason: "scope-deny" });
418
+ return null;
419
+ }
420
+ if (this.scope.allow && !this.scope.allow.some((a) => this.selectorMatchesPattern(sel, a))) {
421
+ this.onEvent?.({
422
+ kind: "skipped",
423
+ adaptation,
424
+ reason: "scope-not-in-allow"
425
+ });
426
+ return null;
427
+ }
428
+ let nodes;
429
+ try {
430
+ if (sel === ":root" || sel === "html") {
431
+ return [document.documentElement];
432
+ }
433
+ nodes = this.root.querySelectorAll(sel);
434
+ } catch {
435
+ this.onEvent?.({
436
+ kind: "skipped",
437
+ adaptation,
438
+ reason: "selector-syntax-error"
439
+ });
440
+ return null;
441
+ }
442
+ return Array.from(nodes).filter(
443
+ (n) => n instanceof HTMLElement
444
+ );
445
+ }
446
+ selectorMatchesPattern(selector, pattern) {
447
+ if (selector === pattern) return true;
448
+ return selector.includes(pattern);
449
+ }
450
+ applyToElement(el, adaptation) {
451
+ switch (adaptation.kind) {
452
+ case "set-css-var": {
453
+ const prev = el.style.getPropertyValue(adaptation.name);
454
+ const prevPriority = el.style.getPropertyPriority(adaptation.name);
455
+ el.style.setProperty(adaptation.name, adaptation.value);
456
+ return () => {
457
+ if (prev) el.style.setProperty(adaptation.name, prev, prevPriority);
458
+ else el.style.removeProperty(adaptation.name);
459
+ };
460
+ }
461
+ case "add-class": {
462
+ if (el.classList.contains(adaptation.className)) return null;
463
+ el.classList.add(adaptation.className);
464
+ return () => el.classList.remove(adaptation.className);
465
+ }
466
+ case "remove-class": {
467
+ if (!el.classList.contains(adaptation.className)) return null;
468
+ el.classList.remove(adaptation.className);
469
+ return () => el.classList.add(adaptation.className);
470
+ }
471
+ case "set-style": {
472
+ const prop = adaptation.property;
473
+ const prev = el.style.getPropertyValue(prop);
474
+ const prevPriority = el.style.getPropertyPriority(prop);
475
+ el.style.setProperty(prop, adaptation.value);
476
+ return () => {
477
+ if (prev) el.style.setProperty(prop, prev, prevPriority);
478
+ else el.style.removeProperty(prop);
479
+ };
480
+ }
481
+ case "set-attribute": {
482
+ const had = el.hasAttribute(adaptation.name);
483
+ const prev = el.getAttribute(adaptation.name);
484
+ el.setAttribute(adaptation.name, adaptation.value);
485
+ return () => {
486
+ if (had && prev !== null) el.setAttribute(adaptation.name, prev);
487
+ else el.removeAttribute(adaptation.name);
488
+ };
489
+ }
490
+ case "set-aria-label": {
491
+ const had = el.hasAttribute("aria-label");
492
+ const prev = el.getAttribute("aria-label");
493
+ el.setAttribute("aria-label", adaptation.value);
494
+ return () => {
495
+ if (had && prev !== null) el.setAttribute("aria-label", prev);
496
+ else el.removeAttribute("aria-label");
497
+ };
498
+ }
499
+ default: {
500
+ const _exhaustive = adaptation;
501
+ void _exhaustive;
502
+ return null;
503
+ }
504
+ }
505
+ }
506
+ };
507
+
508
+ // src/domSnapshot.ts
509
+ function snapshotVisibleSelectors(root, opts = {}) {
510
+ if (typeof window === "undefined" || typeof document === "undefined") {
511
+ return [];
512
+ }
513
+ const max = opts.maxSelectors ?? 50;
514
+ const seen = /* @__PURE__ */ new Set();
515
+ const out = [];
516
+ const viewportH = window.innerHeight || 1;
517
+ const viewportW = window.innerWidth || 1;
518
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
519
+ acceptNode: (node2) => {
520
+ if (!(node2 instanceof HTMLElement)) return NodeFilter.FILTER_SKIP;
521
+ const rect = node2.getBoundingClientRect();
522
+ if (rect.width === 0 || rect.height === 0) return NodeFilter.FILTER_SKIP;
523
+ if (rect.bottom < 0 || rect.top > viewportH) return NodeFilter.FILTER_SKIP;
524
+ if (rect.right < 0 || rect.left > viewportW) return NodeFilter.FILTER_SKIP;
525
+ return NodeFilter.FILTER_ACCEPT;
526
+ }
527
+ });
528
+ let node = walker.nextNode();
529
+ while (node && out.length < max) {
530
+ if (node instanceof HTMLElement) {
531
+ const sel = pickSelector(node);
532
+ if (sel && !seen.has(sel) && passesScope(sel, opts.scope)) {
533
+ seen.add(sel);
534
+ out.push(sel);
535
+ }
536
+ }
537
+ node = walker.nextNode();
538
+ }
539
+ return out;
207
540
  }
208
- function buildDeliberate() {
209
- const events = [];
210
- const productIds = ["p-201", "p-202", "p-203", "p-204", "p-205", "p-206"];
211
- let t = 0;
212
- for (let i = 0; i < 6; i++) {
213
- t += 5500;
214
- const target = productIds[i];
215
- events.push({ kind: "hover", target, durationMs: 1800 + i * 200, t });
216
- t += 200;
217
- events.push({
218
- kind: "dwell",
219
- target,
220
- durationMs: 5e3 + i * 400,
221
- t
222
- });
541
+ function pickSelector(el) {
542
+ if (el.id) return `#${cssEscape(el.id)}`;
543
+ const begenId = el.getAttribute("data-begen-id");
544
+ if (begenId) return `[data-begen-id="${attrEscape(begenId)}"]`;
545
+ const testId = el.getAttribute("data-testid");
546
+ if (testId) return `[data-testid="${attrEscape(testId)}"]`;
547
+ const role = el.getAttribute("role");
548
+ if (role) return `${el.tagName.toLowerCase()}[role="${attrEscape(role)}"]`;
549
+ const semantic = SEMANTIC_TAGS.has(el.tagName.toLowerCase());
550
+ if (semantic) {
551
+ const aria = el.getAttribute("aria-label");
552
+ if (aria) {
553
+ return `${el.tagName.toLowerCase()}[aria-label="${attrEscape(aria)}"]`;
554
+ }
555
+ return el.tagName.toLowerCase();
223
556
  }
224
- for (let i = 0; i < 3; i++) {
225
- t += 4e3;
226
- events.push({ kind: "click", target: productIds[i], t });
557
+ const cls = pickStableClass(el);
558
+ if (cls) return `.${cssEscape(cls)}`;
559
+ return null;
560
+ }
561
+ var SEMANTIC_TAGS = /* @__PURE__ */ new Set([
562
+ "header",
563
+ "main",
564
+ "nav",
565
+ "footer",
566
+ "aside",
567
+ "article",
568
+ "section",
569
+ "form",
570
+ "dialog"
571
+ ]);
572
+ function pickStableClass(el) {
573
+ for (const cls of el.classList) {
574
+ if (cls.includes(":")) continue;
575
+ if (/^[a-z]+-[a-z0-9-]+$/.test(cls)) continue;
576
+ if (cls.length < 3) continue;
577
+ return cls;
227
578
  }
228
- for (let i = 0; i < 5; i++) {
229
- t += 1500;
230
- events.push({
231
- kind: "scroll",
232
- depth: Math.min(0.6, 0.2 + i * 0.08),
233
- t
234
- });
579
+ return null;
580
+ }
581
+ function passesScope(selector, scope) {
582
+ if (!scope) return true;
583
+ if (scope.deny?.some((d) => selector === d || selector.includes(d))) {
584
+ return false;
235
585
  }
236
- return events;
586
+ if (scope.allow && !scope.allow.some((a) => selector === a || selector.includes(a))) {
587
+ return false;
588
+ }
589
+ return true;
237
590
  }
238
- var PERSONAS = {
239
- get decisive() {
240
- return buildDecisive();
241
- },
242
- get deliberate() {
243
- return buildDeliberate();
591
+ function cssEscape(s) {
592
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
593
+ return CSS.escape(s);
244
594
  }
245
- };
595
+ return s.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
596
+ }
597
+ function attrEscape(s) {
598
+ return s.replace(/"/g, '\\"');
599
+ }
246
600
 
247
- // src/BeGenSurface.tsx
248
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
249
- function BeGenSurface(props) {
601
+ // src/BeGenProvider.tsx
602
+ import { jsx } from "react/jsx-runtime";
603
+ var BeGenContext = React.createContext(null);
604
+ function useBeGenContext() {
605
+ const ctx = React.useContext(BeGenContext);
606
+ if (!ctx) {
607
+ throw new Error("useBeGenContext must be used inside <BeGenProvider>");
608
+ }
609
+ return ctx;
610
+ }
611
+ function BeGenProvider(props) {
250
612
  const {
251
- variants,
252
- classify,
253
- variantProps,
613
+ designSystem,
254
614
  pageContext,
255
- seedPersona,
256
- rateLimitMs = 4e3,
257
- className,
258
- style
615
+ classify,
616
+ scope,
617
+ rateLimitMs = 5e3,
618
+ triggerEveryEvents = 5,
619
+ flushAfterMs = 5e3,
620
+ flushEveryEvents = 10,
621
+ bufferSize = 50,
622
+ customListeners,
623
+ containerRef,
624
+ seedTrace,
625
+ children
259
626
  } = props;
260
- const containerRef = React2.useRef(null);
261
- const ctx = React2.useContext(BeGenContext);
262
- const [currentVariant, setCurrentVariant] = React2.useState("neutral");
263
- const [lastDirective, setLastDirective] = React2.useState(null);
264
- const [lastSummary, setLastSummary] = React2.useState(null);
265
- const lastClassifyAtRef = React2.useRef(0);
266
- const inFlightRef = React2.useRef(false);
267
- const classifyRef = React2.useRef(classify);
268
- React2.useEffect(() => {
627
+ const engineRef = React.useRef(null);
628
+ const [summary, setSummary] = React.useState(null);
629
+ const [lastPlan, setLastPlan] = React.useState(null);
630
+ const [appliedAdaptations, setAppliedAdaptations] = React.useState([]);
631
+ const lastClassifyAtRef = React.useRef(0);
632
+ const inFlightRef = React.useRef(false);
633
+ const eventsSinceClassifyRef = React.useRef(0);
634
+ const lastPlanHashRef = React.useRef(null);
635
+ const classifyRef = React.useRef(classify);
636
+ React.useEffect(() => {
269
637
  classifyRef.current = classify;
270
638
  }, [classify]);
271
- const ctxRef = React2.useRef(ctx);
272
- React2.useEffect(() => {
273
- ctxRef.current = ctx;
274
- }, [ctx]);
275
- const handleFlush = React2.useCallback(async (summary) => {
276
- setLastSummary(summary);
277
- ctxRef.current?.setSummary(summary);
278
- const now = Date.now();
279
- if (inFlightRef.current) return;
280
- if (now - lastClassifyAtRef.current < rateLimitMs) return;
281
- lastClassifyAtRef.current = now;
282
- inFlightRef.current = true;
283
- try {
284
- const directive = await classifyRef.current(summary);
285
- setLastDirective(directive);
286
- setCurrentVariant(directive.variant);
287
- ctxRef.current?.setDirective(directive);
288
- ctxRef.current?.setVariant(directive.variant);
289
- } catch {
290
- } finally {
291
- inFlightRef.current = false;
292
- }
293
- }, [rateLimitMs]);
294
- const seedTrace = seedPersona ? PERSONAS[seedPersona] : void 0;
639
+ const designSystemRef = React.useRef(designSystem);
640
+ React.useEffect(() => {
641
+ designSystemRef.current = designSystem;
642
+ }, [designSystem]);
643
+ const pageContextRef = React.useRef(pageContext);
644
+ React.useEffect(() => {
645
+ pageContextRef.current = pageContext;
646
+ }, [pageContext]);
647
+ const scopeRef = React.useRef(scope);
648
+ React.useEffect(() => {
649
+ scopeRef.current = scope;
650
+ }, [scope]);
651
+ React.useEffect(() => {
652
+ if (typeof document === "undefined") return;
653
+ const root = containerRef?.current ?? document.body;
654
+ if (!root) return;
655
+ engineRef.current = new AdaptationEngine({
656
+ root,
657
+ scope: scopeRef.current,
658
+ onEvent: () => {
659
+ if (engineRef.current) {
660
+ setAppliedAdaptations(engineRef.current.getApplied().slice());
661
+ }
662
+ }
663
+ });
664
+ return () => {
665
+ engineRef.current?.revertAll();
666
+ engineRef.current = null;
667
+ };
668
+ }, []);
669
+ const handleFlush = React.useCallback(
670
+ async (s) => {
671
+ setSummary(s);
672
+ eventsSinceClassifyRef.current += 1;
673
+ const fn = classifyRef.current;
674
+ if (!fn) return;
675
+ if (inFlightRef.current) return;
676
+ const now = Date.now();
677
+ if (now - lastClassifyAtRef.current < rateLimitMs) return;
678
+ if (eventsSinceClassifyRef.current < triggerEveryEvents) return;
679
+ lastClassifyAtRef.current = now;
680
+ eventsSinceClassifyRef.current = 0;
681
+ inFlightRef.current = true;
682
+ try {
683
+ const root = containerRef?.current ?? (typeof document !== "undefined" ? document.body : null) ?? null;
684
+ const visibleSelectors = root ? snapshotVisibleSelectors(root, { scope: scopeRef.current }) : [];
685
+ const route = s.page_context?.route ?? "/";
686
+ const input = {
687
+ summary: s,
688
+ designSystem: designSystemRef.current,
689
+ dom: { visibleSelectors, route }
690
+ };
691
+ const plan = await fn(input);
692
+ if (!plan || !Array.isArray(plan.adaptations)) return;
693
+ const hash = hashPlan(plan);
694
+ if (hash === lastPlanHashRef.current) return;
695
+ lastPlanHashRef.current = hash;
696
+ setLastPlan(plan);
697
+ engineRef.current?.apply(plan);
698
+ } catch {
699
+ } finally {
700
+ inFlightRef.current = false;
701
+ }
702
+ },
703
+ [rateLimitMs, triggerEveryEvents, containerRef]
704
+ );
295
705
  useBehaviorTracker({
296
706
  containerRef,
297
707
  onFlush: handleFlush,
298
708
  pageContext,
299
- seedTrace
709
+ seedTrace,
710
+ customListeners,
711
+ flushAfterMs,
712
+ flushEveryEvents,
713
+ bufferSize
300
714
  });
301
- const ActiveComponent = variants[currentVariant] ?? variants.neutral;
302
- return /* @__PURE__ */ jsxs(
303
- "div",
304
- {
305
- ref: containerRef,
306
- className,
307
- style,
308
- "data-begen-surface": true,
309
- "data-begen-variant": currentVariant,
310
- children: [
311
- /* @__PURE__ */ jsx2(
312
- "div",
313
- {
314
- style: {
315
- opacity: 1,
316
- transition: "opacity 200ms ease",
317
- animation: "begen-fade-in 200ms ease"
318
- },
319
- children: ActiveComponent ? /* @__PURE__ */ jsx2(ActiveComponent, { ...variantProps ?? {} }) : null
320
- },
321
- currentVariant
322
- ),
323
- /* @__PURE__ */ jsx2(BeGenSurfaceStyles, {}),
324
- lastDirective && /* @__PURE__ */ jsx2(
325
- "span",
326
- {
327
- style: { display: "none" },
328
- "data-begen-confidence": lastDirective.confidence,
329
- "data-begen-reasoning": lastDirective.reasoning,
330
- "data-begen-events": lastSummary?.events_seen ?? 0
331
- }
332
- )
333
- ]
334
- }
335
- );
336
- }
337
- var STYLE_INJECTED = "__begen_styles__";
338
- function BeGenSurfaceStyles() {
339
- React2.useEffect(() => {
340
- if (typeof document === "undefined") return;
341
- if (document[STYLE_INJECTED]) return;
342
- const tag = document.createElement("style");
343
- tag.setAttribute("data-begen-styles", "");
344
- tag.textContent = `@keyframes begen-fade-in { from { opacity: 0 } to { opacity: 1 } }`;
345
- document.head.appendChild(tag);
346
- document[STYLE_INJECTED] = true;
715
+ const recentEventsRef = React.useRef([]);
716
+ const applyPlan = React.useCallback((plan) => {
717
+ if (!plan || !Array.isArray(plan.adaptations)) return;
718
+ const hash = hashPlan(plan);
719
+ if (hash === lastPlanHashRef.current) return;
720
+ lastPlanHashRef.current = hash;
721
+ setLastPlan(plan);
722
+ engineRef.current?.apply(plan);
347
723
  }, []);
348
- return null;
724
+ const getDesignSystem = React.useCallback(
725
+ () => designSystemRef.current,
726
+ []
727
+ );
728
+ const value = React.useMemo(
729
+ () => ({
730
+ summary,
731
+ lastPlan,
732
+ appliedAdaptations,
733
+ recentEvents: recentEventsRef.current,
734
+ applyPlan,
735
+ getDesignSystem
736
+ }),
737
+ [summary, lastPlan, appliedAdaptations, applyPlan, getDesignSystem]
738
+ );
739
+ return /* @__PURE__ */ jsx(BeGenContext.Provider, { value, children });
349
740
  }
350
-
351
- // src/useBeGenContext.ts
352
- import * as React3 from "react";
353
- function useBeGenContext() {
354
- const ctx = React3.useContext(BeGenContext);
355
- if (!ctx) {
356
- throw new Error("useBeGenContext must be used inside <BeGenProvider>");
357
- }
358
- return ctx;
741
+ function hashPlan(plan) {
742
+ const parts = plan.adaptations.map((a) => {
743
+ switch (a.kind) {
744
+ case "set-css-var":
745
+ return `v|${a.selector}|${a.name}|${a.value}`;
746
+ case "add-class":
747
+ return `+|${a.selector}|${a.className}`;
748
+ case "remove-class":
749
+ return `-|${a.selector}|${a.className}`;
750
+ case "set-style":
751
+ return `s|${a.selector}|${a.property}|${a.value}`;
752
+ case "set-attribute":
753
+ return `a|${a.selector}|${a.name}|${a.value}`;
754
+ case "set-aria-label":
755
+ return `l|${a.selector}|${a.value}`;
756
+ }
757
+ });
758
+ return parts.join("\n");
359
759
  }
360
760
 
361
- // src/classifier/gemini.ts
362
- var CLASSIFIER_SYSTEM_PROMPT = `
363
- You classify e-commerce shopper behavior into UI variants.
364
-
365
- You receive a JSON object describing a user's recent interaction pattern on
366
- a product listing page. Decide which UI variant best serves them right now.
367
-
368
- Variants:
369
- - "decisive": user knows what they want; minimize friction. Dense grid,
370
- prominent prices, fast paths to cart, no recommendations.
371
- Signals: high clicks/min, low dwell, high scroll depth, few hovers.
372
- - "deliberate": user is researching; help them compare. Larger cards,
373
- reviews surfaced inline, "people also viewed", expandable detail.
374
- Signals: low clicks/min, high dwell, hovers across multiple products.
375
- - "neutral": insufficient signal yet, or pattern is mixed. Baseline grid.
376
- Default for first ~10 events.
377
-
378
- Examples:
379
- Input: {"clicks_per_min":14,"avg_dwell_ms":820,"scroll_depth":0.91,"hover_count":2,"events_seen":18}
380
- Output: {"variant":"decisive","confidence":0.86,"reasoning":"Fast clicks, low dwell, deep scroll \u2014 purposeful navigation."}
381
-
382
- Input: {"clicks_per_min":3,"avg_dwell_ms":7400,"scroll_depth":0.42,"hover_count":4,"events_seen":22}
383
- Output: {"variant":"deliberate","confidence":0.81,"reasoning":"Slow pace, long dwell, multi-product hover \u2014 comparing options."}
384
-
385
- Input: {"clicks_per_min":5,"avg_dwell_ms":2100,"scroll_depth":0.55,"hover_count":2,"events_seen":7}
386
- Output: {"variant":"neutral","confidence":0.6,"reasoning":"Not enough events yet to commit to a mode."}
387
-
388
- Return ONLY a JSON object. No prose, no markdown.
389
- `.trim();
390
- var VALID_VARIANTS = /* @__PURE__ */ new Set(["decisive", "deliberate", "neutral"]);
391
- var FALLBACK = {
392
- variant: "neutral",
761
+ // src/adapters/http.ts
762
+ var FALLBACK_PLAN = {
763
+ adaptations: [],
393
764
  confidence: 0,
394
- reasoning: "Classifier error."
765
+ reasoning: "HTTP adapter error."
395
766
  };
396
- function createGeminiClassifier(opts) {
397
- const { apiKey, model = "gemini-2.0-flash", endpoint, fetchImpl } = opts;
398
- const url = endpoint ?? `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`;
767
+ function createHttpAdapter(opts) {
768
+ const {
769
+ url,
770
+ headers,
771
+ fetchImpl,
772
+ timeoutMs = 8e3,
773
+ bodyTransform,
774
+ responseTransform
775
+ } = opts;
399
776
  const fetcher = fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
400
- return async (summary) => {
401
- if (!fetcher) return FALLBACK;
777
+ return async (input) => {
778
+ if (!fetcher) return FALLBACK_PLAN;
779
+ const ac = new AbortController();
780
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
402
781
  try {
403
- const res = await fetcher(`${url}?key=${encodeURIComponent(apiKey)}`, {
782
+ const body = bodyTransform ? bodyTransform(input) : input;
783
+ const res = await fetcher(url, {
404
784
  method: "POST",
405
- headers: { "content-type": "application/json" },
406
- body: JSON.stringify({
407
- systemInstruction: { parts: [{ text: CLASSIFIER_SYSTEM_PROMPT }] },
408
- contents: [
409
- {
410
- role: "user",
411
- parts: [{ text: JSON.stringify(summary) }]
412
- }
413
- ],
414
- generationConfig: {
415
- responseMimeType: "application/json",
416
- temperature: 0.2
417
- }
418
- })
785
+ headers: { "content-type": "application/json", ...headers },
786
+ body: JSON.stringify(body),
787
+ signal: ac.signal
419
788
  });
420
- if (!res.ok) return FALLBACK;
421
- const data = await res.json();
422
- const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
423
- if (!text) return FALLBACK;
424
- const parsed = JSON.parse(text);
425
- if (!parsed || typeof parsed !== "object" || typeof parsed.variant !== "string" || !VALID_VARIANTS.has(parsed.variant) || typeof parsed.reasoning !== "string") {
426
- return FALLBACK;
427
- }
428
- const confidence = typeof parsed.confidence === "number" && Number.isFinite(parsed.confidence) ? Math.max(0, Math.min(1, parsed.confidence)) : 0.5;
429
- return {
430
- variant: parsed.variant,
431
- confidence,
432
- reasoning: parsed.reasoning
433
- };
789
+ if (!res.ok) return FALLBACK_PLAN;
790
+ const raw = await res.json();
791
+ const plan = responseTransform ? responseTransform(raw) : raw;
792
+ if (!isValidPlan(plan)) return FALLBACK_PLAN;
793
+ return plan;
434
794
  } catch {
435
- return FALLBACK;
795
+ return FALLBACK_PLAN;
796
+ } finally {
797
+ clearTimeout(timer);
436
798
  }
437
799
  };
438
800
  }
439
-
440
- // src/classifier/heuristic.ts
441
- function createHeuristicClassifier() {
442
- return async (summary) => {
443
- if (summary.events_seen < 10) {
444
- return {
445
- variant: "neutral",
446
- confidence: 0.5,
447
- reasoning: "Insufficient events."
448
- };
449
- }
450
- if (summary.clicks_per_min > 8 && summary.avg_dwell_ms < 2e3) {
451
- return {
452
- variant: "decisive",
453
- confidence: 0.8,
454
- reasoning: "Fast pace, low dwell."
455
- };
456
- }
457
- if (summary.avg_dwell_ms > 4e3 && summary.hover_count > 2) {
458
- return {
459
- variant: "deliberate",
460
- confidence: 0.8,
461
- reasoning: "Slow pace, multi-hover."
462
- };
463
- }
464
- return {
465
- variant: "neutral",
466
- confidence: 0.6,
467
- reasoning: "Mixed signals."
468
- };
469
- };
801
+ function isValidPlan(p) {
802
+ if (!p || typeof p !== "object") return false;
803
+ const plan = p;
804
+ if (!Array.isArray(plan.adaptations)) return false;
805
+ if (typeof plan.confidence !== "number") return false;
806
+ if (typeof plan.reasoning !== "string") return false;
807
+ return true;
470
808
  }
471
809
  export {
810
+ AdaptationEngine,
472
811
  BeGenProvider,
473
- BeGenSurface,
474
- PERSONAS,
475
- createGeminiClassifier,
476
- createHeuristicClassifier,
812
+ createHttpAdapter,
813
+ snapshotVisibleSelectors,
477
814
  useBeGenContext,
478
815
  useBehaviorTracker
479
816
  };