@reticlehq/browser 3.2.0 → 3.4.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.
Files changed (83) hide show
  1. package/dist/actions/actions-dom.d.ts +6 -5
  2. package/dist/actions/actions-dom.js +5 -4
  3. package/dist/actions/actions-press.d.ts +3 -3
  4. package/dist/actions/actions-press.js +0 -3
  5. package/dist/actions/actions.d.ts +0 -1
  6. package/dist/actions/actions.js +17 -13
  7. package/dist/actions/check-state.d.ts +5 -0
  8. package/dist/actions/check-state.js +24 -0
  9. package/dist/actions/danger-context.d.ts +3 -2
  10. package/dist/actions/danger-context.js +2 -1
  11. package/dist/actions/value-input.d.ts +2 -1
  12. package/dist/actions/value-input.js +15 -6
  13. package/dist/connect-options.d.ts +1 -0
  14. package/dist/dom/a11y.js +43 -3
  15. package/dist/dom/addressing/source.d.ts +2 -1
  16. package/dist/dom/dom-ignore.d.ts +1 -0
  17. package/dist/dom/dom-ignore.js +13 -0
  18. package/dist/dom/query.js +6 -4
  19. package/dist/dom/realm.d.ts +6 -0
  20. package/dist/dom/realm.js +5 -0
  21. package/dist/dom/snapshot.d.ts +1 -1
  22. package/dist/dom/snapshot.js +8 -2
  23. package/dist/observers/console.js +38 -13
  24. package/dist/observers/dom.js +3 -1
  25. package/dist/observers/download.js +3 -2
  26. package/dist/observers/error-repeats.d.ts +18 -0
  27. package/dist/observers/error-repeats.js +41 -0
  28. package/dist/observers/field.d.ts +2 -0
  29. package/dist/observers/field.js +86 -0
  30. package/dist/observers/frames.js +3 -4
  31. package/dist/observers/install-all.js +2 -0
  32. package/dist/observers/net-detail/network-body.d.ts +2 -0
  33. package/dist/observers/net-detail/network-body.js +24 -7
  34. package/dist/observers/network.js +14 -1
  35. package/dist/observers/route.js +4 -4
  36. package/dist/observers/state.js +21 -8
  37. package/dist/presenter/carousel/carousel.d.ts +13 -0
  38. package/dist/presenter/carousel/carousel.js +126 -0
  39. package/dist/presenter/carousel/offer-card.d.ts +21 -0
  40. package/dist/presenter/carousel/offer-card.js +55 -0
  41. package/dist/presenter/carousel/panel-slides.d.ts +7 -0
  42. package/dist/presenter/carousel/panel-slides.js +10 -0
  43. package/dist/presenter/carousel/talk-card.d.ts +8 -0
  44. package/dist/presenter/carousel/talk-card.js +20 -0
  45. package/dist/presenter/chrome/presenter-log.d.ts +2 -1
  46. package/dist/presenter/chrome/presenter-log.js +14 -3
  47. package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -1
  48. package/dist/presenter/chrome/presenter-report-copy.js +1 -1
  49. package/dist/presenter/hud-telemetry.d.ts +3 -0
  50. package/dist/presenter/hud-telemetry.js +87 -0
  51. package/dist/presenter/presenter-account.js +1 -1
  52. package/dist/presenter/presenter-config.d.ts +5 -1
  53. package/dist/presenter/presenter-config.js +4 -1
  54. package/dist/presenter/presenter-controls.d.ts +1 -0
  55. package/dist/presenter/presenter-controls.js +12 -1
  56. package/dist/presenter/presenter-report.js +6 -6
  57. package/dist/presenter/presenter-settings-styles.js +2 -1
  58. package/dist/presenter/presenter-settings.d.ts +2 -4
  59. package/dist/presenter/presenter-settings.js +13 -4
  60. package/dist/presenter/presenter-shell-styles.js +4 -0
  61. package/dist/presenter/presenter-shell.d.ts +4 -1
  62. package/dist/presenter/presenter-shell.js +49 -11
  63. package/dist/presenter/presenter-styles.js +5 -1
  64. package/dist/presenter/presenter-workspace.d.ts +0 -7
  65. package/dist/presenter/presenter-workspace.js +7 -7
  66. package/dist/presenter/presenter.js +43 -10
  67. package/dist/presenter/tour/tour.d.ts +3 -0
  68. package/dist/presenter/tour/tour.js +5 -0
  69. package/dist/recorder/recorder.js +25 -22
  70. package/dist/registry/store-adapters.js +32 -2
  71. package/dist/registry/stores/adapters.js +8 -2
  72. package/dist/reticle-inject.d.ts +2 -0
  73. package/dist/reticle-inject.js +1337 -0
  74. package/dist/reticle.js +6 -0
  75. package/dist/review/annotator.d.ts +3 -1
  76. package/dist/review/annotator.js +37 -40
  77. package/dist/review/marks-for-agent.d.ts +9 -0
  78. package/dist/review/marks-for-agent.js +12 -0
  79. package/dist/util/captured-method.d.ts +0 -1
  80. package/dist/util/captured-method.js +1 -1
  81. package/package.json +15 -11
  82. package/dist/presenter/presenter-offer.d.ts +0 -25
  83. package/dist/presenter/presenter-offer.js +0 -87
package/dist/reticle.js CHANGED
@@ -10,6 +10,7 @@ import { declaredChannels } from './registry/declared-channels.js';
10
10
  import { declaredCommands, declaredPlatform } from './registry/declared-platform.js';
11
11
  import { registerCapabilities, setCapabilitiesListener, hasCapabilities, } from './registry/capabilities.js';
12
12
  import { installAllObservers, runTeardowns } from './observers/install-all.js';
13
+ import { setNetworkBodyMaxChars } from './observers/net-detail/network-body.js';
13
14
  import { installOverlay } from './presenter/chrome/overlay.js';
14
15
  import { LOG_KIND, LOG_RESULT } from './presenter/chrome/log-kinds.js';
15
16
  import { actionVerb } from './presenter/chrome/presenter-verbs.js';
@@ -187,6 +188,9 @@ export class Reticle {
187
188
  });
188
189
  setCapabilitiesListener(() => this.#transport?.reannounce());
189
190
  setPresenterVisible(true === options.exposePresenter);
191
+ if (options.networkBodyMaxChars !== undefined) {
192
+ setNetworkBodyMaxChars(options.networkBodyMaxChars);
193
+ }
190
194
  const emit = this.#emit;
191
195
  this.#captureBodies = true === options.captureNetworkBodies;
192
196
  this.#sourceMapping =
@@ -248,6 +252,7 @@ export class Reticle {
248
252
  panelOptions.onControl = (intent) => this.#emit(EventType.HUMAN_CONTROL, intent.text !== undefined
249
253
  ? { kind: intent.kind, text: intent.text }
250
254
  : { kind: intent.kind });
255
+ panelOptions.onHudUse = (use) => this.#emit(EventType.HUD_USED, { ...use });
251
256
  const panel = new Presenter(panelOptions);
252
257
  this.#presenter = panel;
253
258
  panel.mount();
@@ -263,6 +268,7 @@ export class Reticle {
263
268
  projectId: reticleParamsFromSearch(window.location.search).projectId ?? window.location.origin,
264
269
  isDriving: () => PresenterMode.IDLE !== panel.mode,
265
270
  search: window.location.search,
271
+ navigator,
266
272
  copy: (text) => navigator.clipboard?.writeText(text).then(() => true, () => false) ?? false,
267
273
  select: (element) => {
268
274
  const selection = window.getSelection();
@@ -16,7 +16,9 @@ export interface AnnotatorDeps {
16
16
  export interface AnnotatorChrome {
17
17
  markersBtn?: HTMLElement;
18
18
  clearBtn?: HTMLElement;
19
+ copyRow?: HTMLElement;
19
20
  countEl?: HTMLElement;
21
+ onCount?: (count: number) => void;
20
22
  }
21
23
  export declare class Annotator {
22
24
  #private;
@@ -25,10 +27,10 @@ export declare class Annotator {
25
27
  get markCount(): number;
26
28
  mount(): void;
27
29
  setAccent(hex: string): void;
28
- attachFlagButton(btn: HTMLElement): void;
29
30
  attachChrome(chrome: AnnotatorChrome): void;
30
31
  destroy(): void;
31
32
  toggle(on?: boolean): void;
33
+ agentPrompt(pageUrl: string): string;
32
34
  clearAll(): void;
33
35
  syncAnchors(): void;
34
36
  }
@@ -2,6 +2,7 @@ import { isSyntheticInput } from '../actions/synthetic/synthetic-input.js';
2
2
  import { EventType } from '@reticlehq/core';
3
3
  import { isReticleUi, isReticleOverlay } from '../dom/dom-ignore.js';
4
4
  import { resolveMarkAnchor } from './mark-anchor.js';
5
+ import { marksCountText, marksForAgent } from './marks-for-agent.js';
5
6
  import { nativeSetTimeout, nativeClearTimeout } from '../timers/native/native-timers.js';
6
7
  import { ANNOTATOR_CSS, ANNOTATOR_ROOT_HTML, MARK_CANCEL, MARK_PENDING_GLYPH, MARK_PLACEHOLDER, MARK_SUBMIT, } from './annotator-styles.js';
7
8
  const HIGHLIGHT_REST_MS = 130;
@@ -33,13 +34,11 @@ export class Annotator {
33
34
  #pendingTarget;
34
35
  #markersBtn;
35
36
  #clearBtn;
37
+ #copyRow;
38
+ #onChromeCount;
36
39
  #countEl;
37
40
  #accent;
38
- #onClick;
39
- #onKeydown;
40
- #onMove;
41
- #onScroll;
42
- #onResize;
41
+ #ac;
43
42
  #mo;
44
43
  constructor(deps) {
45
44
  this.#emit = deps.emit;
@@ -69,16 +68,23 @@ export class Annotator {
69
68
  this.#hi = root.querySelector(sel('hi')) ?? undefined;
70
69
  this.#hiLabel = root.querySelector(sel('hilabel')) ?? undefined;
71
70
  this.#selBox = root.querySelector(sel('sel')) ?? undefined;
72
- this.#onClick = (ev) => this.#handleClick(ev);
73
- document.addEventListener('click', this.#onClick, { capture: true });
74
- this.#onMove = (ev) => this.#scheduleMove(ev);
75
- document.addEventListener('mousemove', this.#onMove, { passive: true, capture: true });
76
- this.#onKeydown = (ev) => this.#handleKey(ev);
77
- document.addEventListener('keydown', this.#onKeydown);
78
- this.#onScroll = () => this.#reposition();
79
- this.#onResize = () => this.#reposition();
80
- window.addEventListener('scroll', this.#onScroll, true);
81
- window.addEventListener('resize', this.#onResize);
71
+ const ac = new AbortController();
72
+ this.#ac = ac;
73
+ document.addEventListener('click', (ev) => this.#handleClick(ev), {
74
+ capture: true,
75
+ signal: ac.signal,
76
+ });
77
+ document.addEventListener('mousemove', (ev) => this.#scheduleMove(ev), {
78
+ passive: true,
79
+ capture: true,
80
+ signal: ac.signal,
81
+ });
82
+ document.addEventListener('keydown', (ev) => this.#handleKey(ev), { signal: ac.signal });
83
+ window.addEventListener('scroll', () => this.#reposition(), {
84
+ capture: true,
85
+ signal: ac.signal,
86
+ });
87
+ window.addEventListener('resize', () => this.#reposition(), { signal: ac.signal });
82
88
  this.#mo = new MutationObserver(() => this.syncAnchors());
83
89
  this.#mo.observe(document.documentElement, { childList: true, subtree: true });
84
90
  if (undefined !== this.#accent)
@@ -88,12 +94,11 @@ export class Annotator {
88
94
  this.#accent = hex;
89
95
  this.#root?.style.setProperty('--reticle-mark-accent', hex);
90
96
  }
91
- attachFlagButton(btn) {
92
- this.attachChrome({ markersBtn: btn });
93
- }
94
97
  attachChrome(chrome) {
95
98
  this.#markersBtn = chrome.markersBtn;
96
99
  this.#clearBtn = chrome.clearBtn;
100
+ this.#copyRow = chrome.copyRow;
101
+ this.#onChromeCount = chrome.onCount;
97
102
  this.#countEl = chrome.countEl;
98
103
  this.#markersBtn?.addEventListener('click', (e) => {
99
104
  e.stopPropagation();
@@ -106,26 +111,8 @@ export class Annotator {
106
111
  this.#syncChrome();
107
112
  }
108
113
  destroy() {
109
- if (this.#onClick !== undefined) {
110
- document.removeEventListener('click', this.#onClick, { capture: true });
111
- this.#onClick = undefined;
112
- }
113
- if (this.#onKeydown !== undefined) {
114
- document.removeEventListener('keydown', this.#onKeydown);
115
- this.#onKeydown = undefined;
116
- }
117
- if (this.#onMove !== undefined) {
118
- document.removeEventListener('mousemove', this.#onMove, { capture: true });
119
- this.#onMove = undefined;
120
- }
121
- if (this.#onScroll !== undefined) {
122
- window.removeEventListener('scroll', this.#onScroll, true);
123
- this.#onScroll = undefined;
124
- }
125
- if (this.#onResize !== undefined) {
126
- window.removeEventListener('resize', this.#onResize);
127
- this.#onResize = undefined;
128
- }
114
+ this.#ac?.abort();
115
+ this.#ac = undefined;
129
116
  this.#mo?.disconnect();
130
117
  this.#mo = undefined;
131
118
  if (this.#hiTimer !== undefined)
@@ -151,6 +138,9 @@ export class Annotator {
151
138
  }
152
139
  this.#syncChrome();
153
140
  }
141
+ agentPrompt(pageUrl) {
142
+ return marksForAgent(this.#marks, pageUrl);
143
+ }
154
144
  clearAll() {
155
145
  this.#closePopover();
156
146
  for (const mark of this.#marks)
@@ -193,6 +183,13 @@ export class Annotator {
193
183
  this.#countEl.hidden = 0 === n;
194
184
  }
195
185
  this.#clearBtn?.toggleAttribute('disabled', 0 === n);
186
+ this.#onChromeCount?.(n);
187
+ if (this.#copyRow !== undefined) {
188
+ this.#copyRow.hidden = 0 === n;
189
+ const text = this.#copyRow.querySelector('[data-reticle-marks-text]');
190
+ if (text !== null)
191
+ text.textContent = marksCountText(n);
192
+ }
196
193
  this.#markersBtn?.toggleAttribute('disabled', 0 === n);
197
194
  }
198
195
  #handleKey(ev) {
@@ -344,8 +341,8 @@ export class Annotator {
344
341
  pop.innerHTML = `<div class="reticle-mark-where"></div>
345
342
  <textarea rows="2" placeholder="${MARK_PLACEHOLDER}"></textarea>
346
343
  <div class="reticle-mark-row">
347
- <button type="button" data-cancel>${MARK_CANCEL}</button>
348
- <button type="button" data-send disabled>${MARK_SUBMIT}</button>
344
+ <button type="button" data-cancel data-reticle-mark-cancel>${MARK_CANCEL}</button>
345
+ <button type="button" data-send data-reticle-mark-send disabled>${MARK_SUBMIT}</button>
349
346
  </div>`;
350
347
  const whereEl = pop.querySelector('.reticle-mark-where');
351
348
  if (whereEl !== null)
@@ -0,0 +1,9 @@
1
+ export interface MarkForAgent {
2
+ note: string;
3
+ label: string;
4
+ anchor: string;
5
+ route: string;
6
+ source?: string | undefined;
7
+ }
8
+ export declare function marksCountText(n: number): string;
9
+ export declare function marksForAgent(marks: readonly MarkForAgent[], pageUrl: string): string;
@@ -0,0 +1,12 @@
1
+ export function marksCountText(n) {
2
+ return `${String(n)} ${1 === n ? 'note' : 'notes'} on this page`;
3
+ }
4
+ export function marksForAgent(marks, pageUrl) {
5
+ const n = marks.length;
6
+ const head = `I marked ${String(n)} ${1 === n ? 'issue' : 'issues'} on ${pageUrl} with Reticle. Fix each one, then verify it in the running app:`;
7
+ const rows = marks.map((m, i) => {
8
+ const where = m.source === undefined ? m.anchor : `${m.anchor}, ${m.source}`;
9
+ return `${String(i + 1)}. ${m.note}\n ${m.label} (${where}) on ${m.route}`;
10
+ });
11
+ return [head, '', ...rows].join('\n');
12
+ }
@@ -1,2 +1 @@
1
- export declare function capturedMethod<T>(target: object, key: PropertyKey): T | undefined;
2
1
  export declare function requireCapturedMethod<T>(target: object, key: PropertyKey): T;
@@ -1,4 +1,4 @@
1
- export function capturedMethod(target, key) {
1
+ function capturedMethod(target, key) {
2
2
  return Object.getOwnPropertyDescriptor(target, key)?.value;
3
3
  }
4
4
  export function requireCapturedMethod(target, key) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/browser",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Reticle browser SDK — installs observers, builds semantic snapshots, executes actions, talks to the bridge.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -10,26 +10,30 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "default": "./dist/index.js"
12
12
  },
13
- "./package.json": "./package.json"
13
+ "./package.json": "./package.json",
14
+ "./inject": {
15
+ "types": "./dist/reticle-inject.d.ts",
16
+ "default": "./dist/reticle-inject.js"
17
+ }
14
18
  },
15
19
  "dependencies": {
16
- "@reticlehq/core": "3.2.0"
20
+ "@reticlehq/core": "3.4.0"
17
21
  },
18
22
  "devDependencies": {
19
23
  "@iconify-json/heroicons-outline": "^1.2.1",
20
24
  "@iconify-json/heroicons-solid": "^1.2.1",
21
- "@tanstack/query-core": "^5.101.4",
25
+ "@tanstack/query-core": "^5.103.2",
22
26
  "esbuild": "^0.28.2",
23
- "jotai": "^2.20.2",
27
+ "jotai": "^3.0.0",
24
28
  "jsdom": "^30.0.1",
25
- "mobx": "^7.0.3",
29
+ "mobx": "^7.0.5",
26
30
  "pinia": "^4.0.3",
27
- "react": "^19.2.8",
31
+ "react": "^19.3.0",
28
32
  "recoil": "^0.7.7",
29
- "svelte": "^5.56.10",
33
+ "svelte": "^5.57.1",
30
34
  "valtio": "^2.3.2",
31
- "vue": "^3.5.41",
32
- "xstate": "^5.32.5"
35
+ "vue": "^3.5.43",
36
+ "xstate": "^5.33.2"
33
37
  },
34
38
  "license": "Apache-2.0",
35
39
  "author": "Reticle contributors",
@@ -60,7 +64,7 @@
60
64
  "node": ">=20.0.0"
61
65
  },
62
66
  "scripts": {
63
- "build": "node scripts/embed-heroicons.mjs && tsc -b && node ../../../scripts/alias-dist.mjs",
67
+ "build": "node scripts/embed-heroicons.mjs && tsc -b && node ../../../scripts/alias-dist.mjs && node scripts/bundle-inject.mjs",
64
68
  "typecheck": "tsc -b",
65
69
  "lint": "eslint src",
66
70
  "test:unit": "vitest run src --passWithNoTests",
@@ -1,25 +0,0 @@
1
- export declare const OFFER_ATTR = "data-reticle-offer";
2
- export declare const OFFER_SLOT_ATTR = "data-reticle-offer-slot";
3
- export declare const OFFER_CLAIM_ATTR = "data-reticle-offer-claim";
4
- export declare const OFFER_DISMISS_ATTR = "data-reticle-offer-dismiss";
5
- export declare const OFFER_DISMISSED_KEY = "reticle.harnessOffer.dismissed";
6
- export declare const OFFER_TEXT: {
7
- readonly HEADLINE: "Reticle drives your app for you";
8
- readonly BODY: "The harness explores your app, proves what works, and saves every journey as a test that replays for free. Built on Jev — no model API key needed.";
9
- readonly FREE: "Free for 3 months";
10
- readonly CLAIM: "Claim now";
11
- readonly DISMISS: "Not now";
12
- readonly claimed: (days: number) => string;
13
- readonly ending: (days: number) => string;
14
- };
15
- export declare function offerDismissed(storage: Pick<Storage, 'getItem'> | undefined): boolean;
16
- export declare function dismissOffer(storage: Pick<Storage, 'setItem'> | undefined): void;
17
- export interface OfferState {
18
- claimed: boolean;
19
- daysRemaining?: number | undefined;
20
- claimUrl?: string | undefined;
21
- eligible?: boolean | undefined;
22
- }
23
- export declare function offerHtml(offer: OfferState | undefined, dismissed: boolean): string;
24
- export declare function paintOffer(root: ParentNode, offer: OfferState | undefined, storage: Pick<Storage, 'getItem' | 'setItem'> | undefined): void;
25
- export declare const OFFER_CSS = "\n.reticle-offer{margin:8px 10px;padding:10px 12px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}\n.reticle-offer-head{display:flex;align-items:center;justify-content:space-between;gap:8px;}\n.reticle-offer-title{font-weight:600;font-size:12px;}\n.reticle-offer-free{font-size:10px;padding:2px 6px;border-radius:999px;background:var(--reticle-accent,#4f7cff);color:#fff;white-space:nowrap;}\n.reticle-offer-body{margin:6px 0 8px;font-size:11px;line-height:1.45;opacity:.8;}\n.reticle-offer-actions{display:flex;align-items:center;gap:8px;}\n.reticle-offer-claim{font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;background:var(--reticle-accent,#4f7cff);color:#fff;text-decoration:none;}\n.reticle-offer-dismiss{font-size:11px;padding:4px 8px;border-radius:6px;background:none;border:0;color:inherit;opacity:.6;cursor:pointer;}\n.reticle-offer-dismiss:hover{opacity:1;}\n.reticle-offer-claimed{margin:6px 10px;font-size:10px;opacity:.6;}\n";
@@ -1,87 +0,0 @@
1
- export const OFFER_ATTR = 'data-reticle-offer';
2
- export const OFFER_SLOT_ATTR = 'data-reticle-offer-slot';
3
- export const OFFER_CLAIM_ATTR = 'data-reticle-offer-claim';
4
- export const OFFER_DISMISS_ATTR = 'data-reticle-offer-dismiss';
5
- export const OFFER_DISMISSED_KEY = 'reticle.harnessOffer.dismissed';
6
- export const OFFER_TEXT = {
7
- HEADLINE: 'Reticle drives your app for you',
8
- BODY: 'The harness explores your app, proves what works, and saves every journey as a test that replays for free. Built on Jev — no model API key needed.',
9
- FREE: 'Free for 3 months',
10
- CLAIM: 'Claim now',
11
- DISMISS: 'Not now',
12
- claimed: (days) => `Harness active — ${String(days)} days left`,
13
- ending: (days) => 1 >= days
14
- ? 'Harness free period ends today'
15
- : `Harness free period ends in ${String(days)} days`,
16
- };
17
- const ENDING_SOON_DAYS = 14;
18
- export function offerDismissed(storage) {
19
- try {
20
- return '1' === storage?.getItem(OFFER_DISMISSED_KEY);
21
- }
22
- catch {
23
- return false;
24
- }
25
- }
26
- export function dismissOffer(storage) {
27
- try {
28
- storage?.setItem(OFFER_DISMISSED_KEY, '1');
29
- }
30
- catch {
31
- }
32
- }
33
- const escape = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
34
- export function offerHtml(offer, dismissed) {
35
- if (offer === undefined)
36
- return '';
37
- if (offer.claimed) {
38
- const days = offer.daysRemaining;
39
- if (days === undefined || 0 > days)
40
- return '';
41
- const text = ENDING_SOON_DAYS >= days ? OFFER_TEXT.ending(days) : OFFER_TEXT.claimed(days);
42
- return `<div ${OFFER_ATTR} class="reticle-offer reticle-offer-claimed">${escape(text)}</div>`;
43
- }
44
- if (false === offer.eligible)
45
- return '';
46
- if (dismissed)
47
- return '';
48
- const url = offer.claimUrl;
49
- if (url === undefined || 0 === url.length)
50
- return '';
51
- return `<div ${OFFER_ATTR} class="reticle-offer" role="note">
52
- <div class="reticle-offer-head">
53
- <span class="reticle-offer-title">${escape(OFFER_TEXT.HEADLINE)}</span>
54
- <span class="reticle-offer-free">${escape(OFFER_TEXT.FREE)}</span>
55
- </div>
56
- <p class="reticle-offer-body">${escape(OFFER_TEXT.BODY)}</p>
57
- <div class="reticle-offer-actions">
58
- <a ${OFFER_CLAIM_ATTR} class="reticle-offer-claim" href="${escape(url)}" target="_blank" rel="noopener noreferrer">${escape(OFFER_TEXT.CLAIM)}</a>
59
- <button type="button" ${OFFER_DISMISS_ATTR} class="reticle-offer-dismiss">${escape(OFFER_TEXT.DISMISS)}</button>
60
- </div>
61
- </div>`;
62
- }
63
- export function paintOffer(root, offer, storage) {
64
- const slot = root.querySelector(`[${OFFER_SLOT_ATTR}]`);
65
- if (!(slot instanceof HTMLElement))
66
- return;
67
- slot.innerHTML = offerHtml(offer, offerDismissed(storage));
68
- const dismiss = slot.querySelector(`[${OFFER_DISMISS_ATTR}]`);
69
- dismiss?.addEventListener('click', (event) => {
70
- event.preventDefault();
71
- event.stopPropagation();
72
- dismissOffer(storage);
73
- slot.innerHTML = '';
74
- });
75
- }
76
- export const OFFER_CSS = `
77
- .reticle-offer{margin:8px 10px;padding:10px 12px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}
78
- .reticle-offer-head{display:flex;align-items:center;justify-content:space-between;gap:8px;}
79
- .reticle-offer-title{font-weight:600;font-size:12px;}
80
- .reticle-offer-free{font-size:10px;padding:2px 6px;border-radius:999px;background:var(--reticle-accent,#4f7cff);color:#fff;white-space:nowrap;}
81
- .reticle-offer-body{margin:6px 0 8px;font-size:11px;line-height:1.45;opacity:.8;}
82
- .reticle-offer-actions{display:flex;align-items:center;gap:8px;}
83
- .reticle-offer-claim{font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;background:var(--reticle-accent,#4f7cff);color:#fff;text-decoration:none;}
84
- .reticle-offer-dismiss{font-size:11px;padding:4px 8px;border-radius:6px;background:none;border:0;color:inherit;opacity:.6;cursor:pointer;}
85
- .reticle-offer-dismiss:hover{opacity:1;}
86
- .reticle-offer-claimed{margin:6px 10px;font-size:10px;opacity:.6;}
87
- `;