@7ots/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.env.example +140 -0
  2. package/LICENSE +21 -0
  3. package/README.md +655 -0
  4. package/brand/apple-touch-icon.png +0 -0
  5. package/brand/favicon.svg +4 -0
  6. package/brand/icon-512.png +0 -0
  7. package/brand/mark-mono.svg +3 -0
  8. package/brand/mark.svg +4 -0
  9. package/brand/og-es.png +0 -0
  10. package/brand/og-pt.png +0 -0
  11. package/brand/og.png +0 -0
  12. package/brand/ots.json +11 -0
  13. package/brand/tokens.css +59 -0
  14. package/brand/wordmark.svg +5 -0
  15. package/cli/7ots.mjs +400 -0
  16. package/cli/lib/brain.mjs +143 -0
  17. package/cli/lib/config.mjs +70 -0
  18. package/cli/lib/hooks.mjs +151 -0
  19. package/cli/lib/i18n.mjs +480 -0
  20. package/cli/lib/identity.mjs +51 -0
  21. package/cli/lib/install.mjs +79 -0
  22. package/cli/lib/lines.mjs +73 -0
  23. package/cli/lib/meet.mjs +166 -0
  24. package/cli/lib/open.mjs +19 -0
  25. package/cli/lib/orquesta.mjs +84 -0
  26. package/cli/lib/paths.mjs +49 -0
  27. package/cli/lib/pet-core.mjs +184 -0
  28. package/cli/lib/pet-server.mjs +296 -0
  29. package/cli/lib/prompts.mjs +75 -0
  30. package/cli/lib/terminal.mjs +145 -0
  31. package/cli/lib/ui.mjs +234 -0
  32. package/cli/lib/wizard.mjs +279 -0
  33. package/cli/pet/electron/main.cjs +86 -0
  34. package/cli/pet/pet.html +404 -0
  35. package/dist/7ots.esm.js +627 -0
  36. package/dist/7ots.esm.js.map +7 -0
  37. package/dist/7ots.iife.js +627 -0
  38. package/dist/7ots.iife.js.map +7 -0
  39. package/llms.txt +57 -0
  40. package/package.json +73 -0
  41. package/server/admin.mjs +213 -0
  42. package/server/apuchat-relay.mjs +170 -0
  43. package/server/channels/agent.mjs +136 -0
  44. package/server/channels/apuchat.mjs +364 -0
  45. package/server/channels/apumail.mjs +211 -0
  46. package/server/channels/index.mjs +69 -0
  47. package/server/contact.mjs +151 -0
  48. package/server/demo.mjs +154 -0
  49. package/server/i18n.mjs +54 -0
  50. package/server/identity.mjs +108 -0
  51. package/server/llm.mjs +413 -0
  52. package/server/platform/auth.mjs +290 -0
  53. package/server/platform/crypto.mjs +80 -0
  54. package/server/platform/db.mjs +130 -0
  55. package/server/platform/routes.mjs +388 -0
  56. package/server/platform/runtime.mjs +180 -0
  57. package/server/platform/store.mjs +331 -0
  58. package/server/sdk.mjs +125 -0
  59. package/server/server.mjs +456 -0
  60. package/server/settings.mjs +177 -0
  61. package/server/tts.mjs +186 -0
  62. package/skills/7ots/SKILL.md +59 -0
  63. package/src/actions/ActionRegistry.js +254 -0
  64. package/src/actions/PageTools.js +270 -0
  65. package/src/actions/builtins.js +565 -0
  66. package/src/auth/AuthManager.js +128 -0
  67. package/src/avatar/AvatarStage.js +473 -0
  68. package/src/character/AvatarEditor.js +829 -0
  69. package/src/character/Character.js +1191 -0
  70. package/src/character/motion.js +794 -0
  71. package/src/character/parts.js +559 -0
  72. package/src/context/ContextManager.js +472 -0
  73. package/src/core/AgentBrain.js +341 -0
  74. package/src/core/AgentWidget.js +1059 -0
  75. package/src/core/EventBus.js +61 -0
  76. package/src/core/Proactivity.js +363 -0
  77. package/src/core/storage.js +35 -0
  78. package/src/i18n/index.js +193 -0
  79. package/src/i18n/messages/character.js +543 -0
  80. package/src/i18n/messages/identity.js +141 -0
  81. package/src/i18n/messages/platform.js +114 -0
  82. package/src/i18n/messages/server.js +630 -0
  83. package/src/i18n/messages/widget.js +362 -0
  84. package/src/identity/ContactCard.js +351 -0
  85. package/src/identity/Face.js +148 -0
  86. package/src/identity/random.js +112 -0
  87. package/src/identity/schema.js +262 -0
  88. package/src/index.js +117 -0
  89. package/src/integrations/apuchat.js +212 -0
  90. package/src/integrations/apumail.js +88 -0
  91. package/src/llm/ProxyLLM.js +87 -0
  92. package/src/mcp/McpClient.js +153 -0
  93. package/src/ui/Companion.js +498 -0
  94. package/src/ui/UIManager.js +425 -0
  95. package/src/ui/VirtualPointer.js +746 -0
  96. package/src/ui/markdown.js +94 -0
  97. package/src/voice/VoiceEngine.js +227 -0
@@ -0,0 +1,746 @@
1
+ /**
2
+ * VirtualPointer — el ratón y el teclado del agente.
3
+ *
4
+ * Un cursor visible (con el nombre del agente) se desplaza hasta los elementos y genera la
5
+ * misma secuencia de eventos que un usuario real:
6
+ *
7
+ * ratón pointerover/enter → pointermove → pointerdown/mousedown → focus →
8
+ * pointerup/mouseup → click (· dblclick · contextmenu)
9
+ * teclado keydown → keypress → beforeinput/input (execCommand, compatible con React/Vue) → keyup
10
+ * arrastre pointerdown → dragstart → dragenter/dragover… → drop → dragend → pointerup
11
+ *
12
+ * Límites del navegador (no se pueden saltar desde JavaScript, y es bueno que así sea):
13
+ * - Los eventos son sintéticos (`isTrusted: false`): no mueven el ratón del sistema, no activan
14
+ * `:hover` de CSS, no abren el desplegable nativo de un <select> ni diálogos de archivos.
15
+ * - Las teclas sintéticas no tienen acción por defecto; aquí se emulan las útiles
16
+ * (Enter, Tab, Espacio, Retroceso, flechas, Inicio/Fin, Ctrl+A).
17
+ *
18
+ * El usuario puede detener al agente en cualquier momento con Esc.
19
+ *
20
+ * const p = new VirtualPointer({ name: 'Nube', color: '#4f46e5' });
21
+ * await p.click(el); await p.type(input, 'hola'); await p.press('Enter');
22
+ */
23
+
24
+ import { translator } from '../i18n/index.js';
25
+ import '../i18n/messages/widget.js';
26
+
27
+ const EASE = (t) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
28
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29
+ const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable=""], [contenteditable=true], summary';
30
+
31
+ export class PointerCancelled extends Error {
32
+ constructor() {
33
+ super('El usuario detuvo al asistente (Esc).');
34
+ }
35
+ }
36
+
37
+ export class VirtualPointer {
38
+ /**
39
+ * @param {object} [opt]
40
+ * @param {boolean} [opt.visible=true] false → mismos eventos, sin cursor ni animación
41
+ * @param {number} [opt.speed=1] multiplicador de velocidad (2 = el doble de rápido)
42
+ * @param {string} [opt.name='Asistente']
43
+ * @param {string} [opt.color='#4f46e5']
44
+ * @param {(el: Element) => boolean} [opt.isOwnUi] elementos del propio widget (no cuentan como "tapado")
45
+ * @param {() => void} [opt.onCancel] el usuario pulsó Esc (AgentWidget aborta el turno)
46
+ * @param {(key: string, vars?: object) => string} [opt.t] traductor de la etiqueta del cursor
47
+ */
48
+ constructor({ visible = true, speed = 1, name = null, color = '#4f46e5', isOwnUi = null, onCancel = null, t = translator() } = {}) {
49
+ this.t = t;
50
+ this.visible = visible;
51
+ this.speed = Math.max(0.25, Number(speed) || 1);
52
+ this.name = name || t('widget.defaultName');
53
+ this.color = color;
54
+ this.isOwnUi = isOwnUi || ((el) => !!el?.closest?.('[data-ots-root]'));
55
+ this.onCancel = onCancel;
56
+ this.x = window.innerWidth - 80;
57
+ this.y = window.innerHeight - 80;
58
+ this._hovered = null;
59
+ this._host = null;
60
+ this._hideTimer = null;
61
+ this._busy = 0;
62
+ this._cancelled = false;
63
+ this._onKey = (e) => {
64
+ if (e.isTrusted && e.key === 'Escape' && this._busy) {
65
+ this._cancelled = true;
66
+ this._label(this.t('widget.pointer.halted'));
67
+ this.onCancel?.();
68
+ }
69
+ };
70
+ }
71
+
72
+ /** Sin animación si no hay que mostrarla o la pestaña/usuario no la quieren. */
73
+ get _instant() {
74
+ return !this.visible || document.hidden || matchMedia('(prefers-reduced-motion: reduce)').matches;
75
+ }
76
+
77
+ // ───────────────────────────── API pública ─────────────────────────────
78
+
79
+ /** Mueve el cursor hasta el elemento (lo desplaza a la vista si hace falta). */
80
+ async moveTo(el, { hover = true } = {}) {
81
+ return this._run(async () => {
82
+ const pt = await this._aim(el);
83
+ await this._glide(pt.x, pt.y);
84
+ if (hover) this._hover(pt.hit, pt);
85
+ return pt;
86
+ });
87
+ }
88
+
89
+ async hover(el) {
90
+ return this.moveTo(el, { hover: true });
91
+ }
92
+
93
+ /**
94
+ * Click real (con eventos de puntero, foco y activación).
95
+ * @param {Element} el
96
+ * @param {{button?: 'left'|'right', double?: boolean}} [opt]
97
+ */
98
+ async click(el, { button = 'left', double = false } = {}) {
99
+ return this._run(async () => {
100
+ const pt = await this._aim(el);
101
+ await this._glide(pt.x, pt.y);
102
+ this._hover(pt.hit, pt);
103
+ await this._pause(90);
104
+ const t = pt.hit;
105
+ const btn = button === 'right' ? 2 : 0;
106
+ const clicks = double ? 2 : 1;
107
+ for (let i = 1; i <= clicks; i++) {
108
+ this._check();
109
+ this._ripple();
110
+ const down = this._fire(t, 'pointerdown', pt, { button: btn, buttons: btn === 2 ? 2 : 1 });
111
+ const mdown = this._fire(t, 'mousedown', pt, { button: btn, buttons: btn === 2 ? 2 : 1, detail: i });
112
+ if (down && mdown) focusFrom(t);
113
+ await this._pause(60);
114
+ this._fire(t, 'pointerup', pt, { button: btn });
115
+ this._fire(t, 'mouseup', pt, { button: btn, detail: i });
116
+ if (btn === 2) {
117
+ this._fire(t, 'contextmenu', pt, { button: 2, detail: i });
118
+ } else {
119
+ // Un click sintético vía dispatchEvent ejecuta la activación (enlaces, checkbox, submit).
120
+ this._fire(t, 'click', pt, { detail: i });
121
+ }
122
+ if (i < clicks) await this._pause(70);
123
+ }
124
+ if (double && btn === 0) this._fire(t, 'dblclick', pt, { detail: 2 });
125
+ await this._pause(120);
126
+ return t;
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Escribe como un usuario: tecla a tecla, con los eventos de teclado e input.
132
+ * @param {Element|null} el null → el elemento que tiene el foco
133
+ * @param {string} text '\n' en un textarea = salto de línea
134
+ * @param {{clear?: boolean}} [opt]
135
+ */
136
+ async type(el, text, { clear = false } = {}) {
137
+ return this._run(async () => {
138
+ let target = el || deepActive();
139
+ if (!target || target === document.body) throw new Error('No hay ningún campo con el foco. Indica el campo (target).');
140
+ if (el) {
141
+ const pt = await this._aim(el);
142
+ await this._glide(pt.x, pt.y);
143
+ this._hover(pt.hit, pt);
144
+ }
145
+ if (deepActive() !== target) {
146
+ this._ripple();
147
+ focusFrom(target);
148
+ }
149
+ target = deepActive() || target;
150
+ if (!isEditable(target)) throw new Error('Ese elemento no admite texto.');
151
+ if (clear) {
152
+ selectAll(target);
153
+ if (!document.execCommand('delete')) setNativeValue(target, '');
154
+ }
155
+ const chars = [...String(text)];
156
+ // Rápido pero visible: ~35 ms por tecla, y como mucho ~2,5 s en total.
157
+ const per = this._instant ? 0 : Math.min(35, 2500 / Math.max(1, chars.length)) / this.speed;
158
+ this._label(this.t('widget.pointer.typing'));
159
+ for (const ch of chars) {
160
+ this._check();
161
+ if (ch === '\n' && target.tagName !== 'TEXTAREA' && !target.isContentEditable) {
162
+ await this._pressIn(target, 'Enter');
163
+ } else {
164
+ await this._key(target, ch, () => insertText(target, ch));
165
+ }
166
+ if (per) await sleep(per * (0.6 + Math.random() * 0.8));
167
+ }
168
+ this._label(null);
169
+ return target;
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Pulsa una tecla o combinación: "Enter", "Tab", "Shift+Tab", "Escape", "ArrowDown",
175
+ * "Control+a", "Backspace"… sobre el elemento con foco (o `el` si se da).
176
+ */
177
+ async press(combo, el = null) {
178
+ return this._run(async () => {
179
+ if (el) {
180
+ const pt = await this._aim(el);
181
+ await this._glide(pt.x, pt.y);
182
+ if (deepActive() !== el) focusFrom(el);
183
+ }
184
+ const target = deepActive() || document.body;
185
+ await this._pressIn(target, combo);
186
+ await this._pause(80);
187
+ return deepActive();
188
+ });
189
+ }
190
+
191
+ /** Arrastra `src` y lo suelta sobre `dst` (HTML5 drag & drop y librerías de pointer events). */
192
+ async drag(src, dst) {
193
+ return this._run(async () => {
194
+ const a = await this._aim(src);
195
+ await this._glide(a.x, a.y);
196
+ this._hover(a.hit, a);
197
+ await this._pause(80);
198
+ this._fire(a.hit, 'pointerdown', a, { buttons: 1 });
199
+ this._fire(a.hit, 'mousedown', a, { buttons: 1 });
200
+ const dt = new DataTransfer();
201
+ const draggable = a.hit.closest?.('[draggable=true]') || (src.draggable ? src : null);
202
+ let dragOk = false;
203
+ if (draggable) dragOk = this._fireDrag(draggable, 'dragstart', a, dt);
204
+
205
+ // El destino puede no estar en pantalla: se calcula tras el scroll del origen.
206
+ const r = dst.getBoundingClientRect();
207
+ const b = { x: r.left + r.width / 2, y: r.top + Math.min(r.height / 2, 40) };
208
+ const steps = this._instant ? 2 : 14;
209
+ let over = null;
210
+ for (let i = 1; i <= steps; i++) {
211
+ this._check();
212
+ const x = a.x + (b.x - a.x) * EASE(i / steps);
213
+ const y = a.y + (b.y - a.y) * EASE(i / steps);
214
+ this._place(x, y);
215
+ const hit = this._hitAt(x, y) || dst;
216
+ const pt = { x, y };
217
+ this._fire(hit, 'pointermove', pt, { buttons: 1 });
218
+ this._fire(hit, 'mousemove', pt, { buttons: 1 });
219
+ if (dragOk) {
220
+ if (hit !== over) {
221
+ if (over) this._fireDrag(over, 'dragleave', pt, dt);
222
+ this._fireDrag(hit, 'dragenter', pt, dt);
223
+ over = hit;
224
+ }
225
+ this._fireDrag(hit, 'dragover', pt, dt);
226
+ }
227
+ if (!this._instant) await sleep(28 / this.speed);
228
+ }
229
+ const end = this._hitAt(b.x, b.y) || dst;
230
+ if (dragOk) {
231
+ this._fireDrag(end, 'drop', b, dt);
232
+ this._fireDrag(draggable, 'dragend', b, dt);
233
+ }
234
+ this._fire(end, 'pointerup', b);
235
+ this._fire(end, 'mouseup', b);
236
+ this._ripple();
237
+ await this._pause(150);
238
+ return end;
239
+ });
240
+ }
241
+
242
+ /** Rueda del ratón sobre un elemento (o la página): desplaza su contenedor desplazable. */
243
+ async scroll(direction = 'down', amount = 1, el = null) {
244
+ return this._run(async () => {
245
+ let pt = { x: window.innerWidth / 2, y: window.innerHeight / 2, hit: document.scrollingElement };
246
+ if (el) {
247
+ pt = await this._aim(el);
248
+ await this._glide(pt.x, pt.y);
249
+ }
250
+ const dy = direction === 'up' ? -1 : direction === 'down' ? 1 : 0;
251
+ const dx = direction === 'left' ? -1 : direction === 'right' ? 1 : 0;
252
+ const box = scrollableFrom(pt.hit, dx ? 'x' : 'y');
253
+ const page = box === document.scrollingElement || box === document.body;
254
+ const size = page ? window.innerHeight : box.clientHeight;
255
+ const px = Math.round(size * 0.8 * Math.min(10, Math.max(0.1, amount)));
256
+ const ok = this._fire(pt.hit, 'wheel', pt, { deltaX: dx * px, deltaY: dy * px, deltaMode: 0 }, WheelEvent);
257
+ if (ok) {
258
+ const opts = { left: dx * px, top: dy * px, behavior: this._instant ? 'instant' : 'smooth' };
259
+ page ? window.scrollBy(opts) : box.scrollBy(opts);
260
+ }
261
+ await this._pause(350);
262
+ return box;
263
+ });
264
+ }
265
+
266
+ /** Oculta el cursor ya (p. ej. al cerrar el widget). */
267
+ hide() {
268
+ clearTimeout(this._hideTimer);
269
+ this._host?.classList.remove('on');
270
+ }
271
+
272
+ destroy() {
273
+ document.removeEventListener('keydown', this._onKey, true);
274
+ this._host?.remove();
275
+ this._host = null;
276
+ }
277
+
278
+ // ───────────────────────────── internos ─────────────────────────────
279
+
280
+ async _run(fn) {
281
+ if (!this._busy) this._cancelled = false;
282
+ this._busy++;
283
+ document.addEventListener('keydown', this._onKey, true);
284
+ this._show();
285
+ try {
286
+ this._check();
287
+ return await fn();
288
+ } finally {
289
+ this._busy--;
290
+ if (!this._busy) {
291
+ document.removeEventListener('keydown', this._onKey, true);
292
+ this._scheduleHide();
293
+ }
294
+ }
295
+ }
296
+
297
+ _check() {
298
+ if (this._cancelled) throw new PointerCancelled();
299
+ }
300
+
301
+ async _pause(ms) {
302
+ this._check();
303
+ if (!this._instant) await sleep(ms / this.speed);
304
+ this._check();
305
+ }
306
+
307
+ /**
308
+ * Lleva el elemento a la vista y calcula el punto donde "pulsar".
309
+ * Comprueba qué hay realmente bajo ese punto: si otro elemento lo tapa (banner de cookies,
310
+ * modal del sitio), falla con un mensaje útil en lugar de pulsar a ciegas.
311
+ */
312
+ async _aim(el) {
313
+ if (!el?.isConnected) throw new Error('El elemento ya no está en la página. Vuelve a leer el contexto.');
314
+ const target = visualTarget(el);
315
+ let r = target.getBoundingClientRect();
316
+ const inView = r.top >= 0 && r.bottom <= window.innerHeight && r.left >= 0 && r.right <= window.innerWidth;
317
+ if (!inView) {
318
+ target.scrollIntoView({ behavior: this._instant ? 'instant' : 'smooth', block: 'center', inline: 'nearest' });
319
+ r = await settle(target, this._instant);
320
+ }
321
+ if (r.width === 0 && r.height === 0) throw new Error('El elemento no es visible (quizá está en un menú o pestaña cerrados).');
322
+ const x = clamp(r.left + Math.min(r.width / 2, 60), 1, window.innerWidth - 2);
323
+ const y = clamp(r.top + r.height / 2, 1, window.innerHeight - 2);
324
+ const hit = this._hitAt(x, y);
325
+ const ok = hit && (hit === target || target.contains(hit) || hit.contains(target) || labelFor(hit) === el || el.contains(hit));
326
+ if (!ok) {
327
+ if (!hit || this.isOwnUi(hit)) return { x, y, hit: target }; // tapado por el propio widget: da igual
328
+ const cover = describe(hit);
329
+ throw new Error(`Hay algo encima del elemento (${cover}). Ciérralo o desplázate antes de intentarlo.`);
330
+ }
331
+ return { x, y, hit };
332
+ }
333
+
334
+ _hitAt(x, y) {
335
+ let hit = document.elementFromPoint(x, y);
336
+ // Atraviesa shadow roots abiertos (web components del sitio).
337
+ while (hit?.shadowRoot && !this.isOwnUi(hit)) {
338
+ const inner = hit.shadowRoot.elementFromPoint(x, y);
339
+ if (!inner || inner === hit) break;
340
+ hit = inner;
341
+ }
342
+ return hit;
343
+ }
344
+
345
+ async _glide(x, y) {
346
+ const x0 = this.x;
347
+ const y0 = this.y;
348
+ const dist = Math.hypot(x - x0, y - y0);
349
+ if (this._instant || dist < 2) return this._place(x, y);
350
+ const dur = Math.min(700, 180 + dist * 0.45) / this.speed;
351
+ const start = performance.now();
352
+ await new Promise((resolve) => {
353
+ const tick = (now) => {
354
+ if (this._cancelled) return resolve();
355
+ const t = Math.min(1, (now - start) / dur);
356
+ this._place(x0 + (x - x0) * EASE(t), y0 + (y - y0) * EASE(t));
357
+ t < 1 ? requestAnimationFrame(tick) : resolve();
358
+ };
359
+ requestAnimationFrame(tick);
360
+ });
361
+ this._check();
362
+ this._place(x, y);
363
+ }
364
+
365
+ _place(x, y) {
366
+ this.x = x;
367
+ this.y = y;
368
+ if (this._host) this._host.style.transform = `translate(${x}px, ${y}px)`;
369
+ }
370
+
371
+ /** Eventos de entrada/salida al pasar de un elemento a otro. */
372
+ _hover(el, pt) {
373
+ const prev = this._hovered?.isConnected ? this._hovered : null;
374
+ if (prev === el) {
375
+ this._fire(el, 'pointermove', pt);
376
+ this._fire(el, 'mousemove', pt);
377
+ return;
378
+ }
379
+ if (prev) {
380
+ this._fire(prev, 'pointerout', pt, { relatedTarget: el });
381
+ this._fire(prev, 'mouseout', pt, { relatedTarget: el });
382
+ for (const n of chain(prev)) if (!n.contains(el)) {
383
+ this._fire(n, 'pointerleave', pt, { bubbles: false });
384
+ this._fire(n, 'mouseleave', pt, { bubbles: false });
385
+ }
386
+ }
387
+ this._fire(el, 'pointerover', pt, { relatedTarget: prev });
388
+ this._fire(el, 'mouseover', pt, { relatedTarget: prev });
389
+ for (const n of chain(el).reverse()) if (!prev || !n.contains(prev)) {
390
+ this._fire(n, 'pointerenter', pt, { bubbles: false });
391
+ this._fire(n, 'mouseenter', pt, { bubbles: false });
392
+ }
393
+ this._fire(el, 'pointermove', pt);
394
+ this._fire(el, 'mousemove', pt);
395
+ this._hovered = el;
396
+ }
397
+
398
+ /** @returns {boolean} false si el sitio canceló el evento (preventDefault) */
399
+ _fire(el, type, pt, extra = {}, Ctor = null) {
400
+ const base = {
401
+ bubbles: true, cancelable: true, composed: true, view: window,
402
+ clientX: pt.x, clientY: pt.y, screenX: pt.x + window.screenX, screenY: pt.y + window.screenY,
403
+ button: 0, buttons: 0, detail: 0,
404
+ };
405
+ const C = Ctor || (type.startsWith('pointer') ? (window.PointerEvent || MouseEvent) : MouseEvent);
406
+ const init = type.startsWith('pointer')
407
+ ? { ...base, pointerId: 1, pointerType: 'mouse', isPrimary: true, width: 1, height: 1, pressure: /down/.test(type) ? 0.5 : 0, ...extra }
408
+ : { ...base, ...extra };
409
+ return el.dispatchEvent(new C(type, init));
410
+ }
411
+
412
+ _fireDrag(el, type, pt, dataTransfer) {
413
+ const ev = new DragEvent(type, {
414
+ bubbles: true, cancelable: true, composed: true, clientX: pt.x, clientY: pt.y, dataTransfer,
415
+ });
416
+ const notCancelled = el.dispatchEvent(ev);
417
+ // dragstart cancelado = no se arrastra. drop/dragover cancelados = el sitio lo aceptó.
418
+ return type === 'dragstart' ? notCancelled : !notCancelled;
419
+ }
420
+
421
+ /** Secuencia de teclado alrededor de una acción (insertar texto, borrar…). */
422
+ async _key(target, key, action, mods = {}) {
423
+ const init = { key, code: codeOf(key), bubbles: true, cancelable: true, composed: true, ...mods };
424
+ const down = target.dispatchEvent(new KeyboardEvent('keydown', init));
425
+ let ok = down;
426
+ if (down && key.length === 1 && !mods.ctrlKey && !mods.metaKey) {
427
+ ok = target.dispatchEvent(new KeyboardEvent('keypress', { ...init, charCode: key.charCodeAt(0) }));
428
+ }
429
+ if (ok && action) action();
430
+ target.dispatchEvent(new KeyboardEvent('keyup', init));
431
+ return ok;
432
+ }
433
+
434
+ /** Pulsa una combinación y emula su acción por defecto (las teclas sintéticas no la tienen). */
435
+ async _pressIn(target, combo) {
436
+ const parts = String(combo).split('+').map((s) => s.trim()).filter(Boolean);
437
+ const key = normalizeKey(parts.pop() || '');
438
+ const mods = {
439
+ ctrlKey: parts.some((p) => /^(ctrl|control)$/i.test(p)),
440
+ shiftKey: parts.some((p) => /^shift$/i.test(p)),
441
+ altKey: parts.some((p) => /^(alt|option)$/i.test(p)),
442
+ metaKey: parts.some((p) => /^(meta|cmd|command)$/i.test(p)),
443
+ };
444
+ this._label(combo);
445
+ this._check();
446
+ await this._key(target, key, () => defaultAction(target, key, mods), mods);
447
+ this._label(null);
448
+ }
449
+
450
+ // ───────────────────────── dibujo del cursor ─────────────────────────
451
+
452
+ _mount() {
453
+ if (this._host || !this.visible) return;
454
+ const host = document.createElement('div');
455
+ host.setAttribute('data-ots-root', 'pointer');
456
+ host.setAttribute('aria-hidden', 'true');
457
+ // popover="manual" lo lleva a la capa superior: así se dibuja también encima de un <dialog> modal del sitio.
458
+ if ('popover' in host) host.popover = 'manual';
459
+ host.style.cssText =
460
+ 'position:fixed;inset:auto;left:0;top:0;margin:0;padding:0;border:0;width:0;height:0;overflow:visible;background:none;' +
461
+ 'z-index:2147483647;pointer-events:none;will-change:transform;';
462
+ const root = host.attachShadow({ mode: 'open' });
463
+ root.innerHTML = `<style>
464
+ :host { all: initial; }
465
+ .c { position: absolute; left: -3px; top: -2px; opacity: 0; transition: opacity .2s; filter: drop-shadow(0 2px 3px rgba(0,0,0,.35)); }
466
+ :host(.on) .c { opacity: 1; }
467
+ .tag { position: absolute; left: 18px; top: 20px; white-space: nowrap; font: 600 12px/1 system-ui, sans-serif;
468
+ color: #fff; background: ${this.color}; padding: 5px 8px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.25); }
469
+ .tag small { font-weight: 400; opacity: .85; margin-left: 6px; }
470
+ .ring { position: absolute; left: -14px; top: -14px; width: 28px; height: 28px; border-radius: 50%;
471
+ border: 2px solid ${this.color}; opacity: 0; transform: scale(.3); }
472
+ .ring.go { animation: ring .45s ease-out; }
473
+ @keyframes ring { 0% { opacity: .9; transform: scale(.3); } 100% { opacity: 0; transform: scale(1.6); } }
474
+ @media (prefers-reduced-motion: reduce) { .ring.go { animation: none; } }
475
+ </style>
476
+ <div class="ring"></div>
477
+ <div class="c">
478
+ <svg width="22" height="26" viewBox="0 0 22 26"><path d="M2 2 L2 21 L7.5 16 L11 24 L14.5 22.5 L11 15 L18.5 15 Z"
479
+ fill="${this.color}" stroke="#fff" stroke-width="1.6" stroke-linejoin="round"/></svg>
480
+ <div class="tag"><span class="n"></span><small class="s"></small></div>
481
+ </div>`;
482
+ root.querySelector('.n').textContent = this.name;
483
+ root.querySelector('.s').textContent = this.t('widget.pointer.hint');
484
+ document.documentElement.appendChild(host);
485
+ this._host = host;
486
+ this._place(this.x, this.y);
487
+ }
488
+
489
+ _show() {
490
+ if (!this.visible) return;
491
+ this._mount();
492
+ this._host.shadowRoot.querySelector('.n').textContent = this.name; // por si cambió (identidad nueva)
493
+ clearTimeout(this._hideTimer);
494
+ this._raise();
495
+ this._host.classList.add('on');
496
+ }
497
+
498
+ /** Vuelve a ponerse el último de la capa superior (por si el sitio abrió un modal después). */
499
+ _raise() {
500
+ const h = this._host;
501
+ if (!h?.showPopover) return;
502
+ try {
503
+ if (h.matches(':popover-open')) {
504
+ if (!document.querySelector('dialog:modal')) return;
505
+ h.hidePopover();
506
+ }
507
+ h.showPopover();
508
+ } catch {}
509
+ }
510
+
511
+ _scheduleHide() {
512
+ clearTimeout(this._hideTimer);
513
+ this._hideTimer = setTimeout(() => this._host?.classList.remove('on'), 2500);
514
+ }
515
+
516
+ _label(text) {
517
+ const s = this._host?.shadowRoot.querySelector('.s');
518
+ if (s) s.textContent = text || this.t('widget.pointer.hint');
519
+ }
520
+
521
+ _ripple() {
522
+ const ring = this._host?.shadowRoot.querySelector('.ring');
523
+ if (!ring) return;
524
+ ring.classList.remove('go');
525
+ void ring.offsetWidth; // reinicia la animación
526
+ ring.classList.add('go');
527
+ }
528
+ }
529
+
530
+ // ───────────────────────────── helpers ─────────────────────────────
531
+
532
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
533
+
534
+ /** Espera a que termine el scroll suave (posición estable) y devuelve el rect final. */
535
+ async function settle(el, instant) {
536
+ let r = el.getBoundingClientRect();
537
+ if (instant) return r;
538
+ for (let i = 0; i < 30; i++) {
539
+ await sleep(40);
540
+ const n = el.getBoundingClientRect();
541
+ if (Math.abs(n.top - r.top) < 1 && Math.abs(n.left - r.left) < 1) return n;
542
+ r = n;
543
+ }
544
+ return r;
545
+ }
546
+
547
+ /** Checkbox/radio ocultos (estilo "switch"): se pulsa su <label> visible. */
548
+ function visualTarget(el) {
549
+ const r = el.getBoundingClientRect();
550
+ if ((r.width < 2 || r.height < 2 || getComputedStyle(el).opacity === '0') && el.labels?.length) return el.labels[0];
551
+ return el;
552
+ }
553
+
554
+ function labelFor(node) {
555
+ const label = node.closest?.('label');
556
+ return label?.control || null;
557
+ }
558
+
559
+ function chain(el) {
560
+ const out = [];
561
+ for (let n = el; n && n.nodeType === 1; n = n.parentElement || n.getRootNode()?.host) out.push(n);
562
+ return out;
563
+ }
564
+
565
+ function describe(el) {
566
+ const text = (el.getAttribute?.('aria-label') || el.innerText || '').trim().replace(/\s+/g, ' ').slice(0, 50);
567
+ const id = el.id ? `#${el.id}` : '';
568
+ return `${el.tagName.toLowerCase()}${id}${text ? ` «${text}»` : ''}`;
569
+ }
570
+
571
+ function deepActive() {
572
+ let a = document.activeElement;
573
+ while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement;
574
+ return a;
575
+ }
576
+
577
+ function focusFrom(el) {
578
+ const f = el.closest?.(FOCUSABLE) || (el.matches?.(FOCUSABLE) ? el : null);
579
+ if (f && deepActive() !== f) f.focus({ preventScroll: true });
580
+ }
581
+
582
+ function isEditable(el) {
583
+ if (el.isContentEditable) return true;
584
+ if (el.tagName === 'TEXTAREA') return !el.readOnly && !el.disabled;
585
+ if (el.tagName !== 'INPUT') return false;
586
+ return !el.readOnly && !el.disabled && /^(text|search|email|url|tel|number|password|date|time|datetime-local|month|week)?$/.test(el.type === 'text' ? '' : el.type);
587
+ }
588
+
589
+ function selectAll(el) {
590
+ if (el.select) el.select();
591
+ else if (el.isContentEditable) {
592
+ const range = document.createRange();
593
+ range.selectNodeContents(el);
594
+ const sel = getSelection();
595
+ sel.removeAllRanges();
596
+ sel.addRange(range);
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Inserta texto en la posición del cursor. execCommand genera beforeinput/input "reales" que
602
+ * React, Vue y los editores entienden; si el navegador no lo permite, se usa el setter nativo.
603
+ */
604
+ function insertText(el, text) {
605
+ const before = el.value ?? el.textContent;
606
+ let ok = false;
607
+ try {
608
+ ok = document.execCommand('insertText', false, text);
609
+ } catch {}
610
+ const after = el.value ?? el.textContent;
611
+ if (ok && after !== before) return;
612
+ if (el.isContentEditable) {
613
+ el.textContent += text;
614
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
615
+ return;
616
+ }
617
+ let start = null;
618
+ try { start = el.selectionStart; } catch {}
619
+ const v = el.value;
620
+ if (start == null) setNativeValue(el, v + text);
621
+ else {
622
+ const end = el.selectionEnd;
623
+ setNativeValue(el, v.slice(0, start) + text + v.slice(end));
624
+ try { el.setSelectionRange(start + text.length, start + text.length); } catch {}
625
+ }
626
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
627
+ }
628
+
629
+ function setNativeValue(el, value) {
630
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : el.tagName === 'SELECT' ? HTMLSelectElement.prototype : HTMLInputElement.prototype;
631
+ Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, value);
632
+ }
633
+
634
+ function tabbables() {
635
+ return [...document.querySelectorAll(FOCUSABLE)].filter((el) => {
636
+ if (el.closest('[data-ots-root], [inert], [hidden]')) return false;
637
+ const r = el.getBoundingClientRect();
638
+ return r.width > 0 && r.height > 0 && getComputedStyle(el).visibility !== 'hidden';
639
+ });
640
+ }
641
+
642
+ function scrollableFrom(el, axis) {
643
+ for (let n = el; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
644
+ const s = getComputedStyle(n);
645
+ const over = axis === 'x' ? s.overflowX : s.overflowY;
646
+ const can = axis === 'x' ? n.scrollWidth > n.clientWidth : n.scrollHeight > n.clientHeight;
647
+ if (can && /(auto|scroll|overlay)/.test(over)) return n;
648
+ }
649
+ return document.scrollingElement || document.documentElement;
650
+ }
651
+
652
+ const KEY_ALIASES = {
653
+ enter: 'Enter', return: 'Enter', tab: 'Tab', esc: 'Escape', escape: 'Escape', space: ' ', spacebar: ' ',
654
+ backspace: 'Backspace', delete: 'Delete', supr: 'Delete', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft',
655
+ right: 'ArrowRight', arrowup: 'ArrowUp', arrowdown: 'ArrowDown', arrowleft: 'ArrowLeft', arrowright: 'ArrowRight',
656
+ home: 'Home', end: 'End', pageup: 'PageUp', pagedown: 'PageDown',
657
+ };
658
+
659
+ function normalizeKey(k) {
660
+ return KEY_ALIASES[k.toLowerCase()] || (k.length === 1 ? k : k[0].toUpperCase() + k.slice(1));
661
+ }
662
+
663
+ function codeOf(key) {
664
+ if (/^[a-z]$/i.test(key)) return `Key${key.toUpperCase()}`;
665
+ if (/^\d$/.test(key)) return `Digit${key}`;
666
+ if (key === ' ') return 'Space';
667
+ if (key.length > 1) return key;
668
+ return '';
669
+ }
670
+
671
+ /** Lo que haría el navegador con una tecla real. */
672
+ function defaultAction(el, key, m) {
673
+ const tag = el.tagName;
674
+ const text = isEditable(el);
675
+ if ((m.ctrlKey || m.metaKey) && key.toLowerCase() === 'a') return selectAll(el);
676
+ if (m.ctrlKey || m.metaKey || m.altKey) return;
677
+ switch (key) {
678
+ case 'Enter':
679
+ if (tag === 'TEXTAREA' || el.isContentEditable) return insertText(el, '\n');
680
+ if (tag === 'INPUT' && el.form && !/^(checkbox|radio|button|submit|reset)$/.test(el.type)) return el.form.requestSubmit ? el.form.requestSubmit() : el.form.submit();
681
+ if (el.matches('a[href], button, summary, [role=button], [role=link], [role=tab], [role=menuitem], input[type=submit], input[type=button]')) return el.click();
682
+ return;
683
+ case ' ':
684
+ if (text) return insertText(el, ' ');
685
+ if (el.matches('button, summary, [role=button], [role=checkbox], [role=switch], [role=tab], input[type=checkbox], input[type=radio]')) return el.click();
686
+ return window.scrollBy({ top: window.innerHeight * 0.8 });
687
+ case 'Tab': {
688
+ const list = tabbables();
689
+ const i = list.indexOf(el);
690
+ const next = list[(i + (m.shiftKey ? -1 : 1) + list.length) % list.length];
691
+ return next?.focus();
692
+ }
693
+ case 'Backspace':
694
+ if (text) return document.execCommand('delete') || deleteChar(el, -1);
695
+ return;
696
+ case 'Delete':
697
+ if (text) return document.execCommand('forwardDelete') || deleteChar(el, 1);
698
+ return;
699
+ case 'ArrowUp':
700
+ case 'ArrowDown': {
701
+ const dir = key === 'ArrowDown' ? 1 : -1;
702
+ if (tag === 'SELECT') {
703
+ const i = clamp(el.selectedIndex + dir, 0, el.options.length - 1);
704
+ if (i !== el.selectedIndex) {
705
+ el.selectedIndex = i;
706
+ el.dispatchEvent(new Event('input', { bubbles: true }));
707
+ el.dispatchEvent(new Event('change', { bubbles: true }));
708
+ }
709
+ return;
710
+ }
711
+ if (tag === 'INPUT' && (el.type === 'number' || el.type === 'range')) return stepInput(el, dir);
712
+ if (!text) return window.scrollBy({ top: dir * 60 });
713
+ return;
714
+ }
715
+ case 'ArrowLeft':
716
+ case 'ArrowRight':
717
+ if (tag === 'INPUT' && el.type === 'range') return stepInput(el, key === 'ArrowRight' ? 1 : -1);
718
+ return;
719
+ case 'PageDown':
720
+ case 'PageUp':
721
+ if (!text) window.scrollBy({ top: (key === 'PageDown' ? 1 : -1) * window.innerHeight * 0.85 });
722
+ return;
723
+ case 'Home':
724
+ case 'End':
725
+ if (!text) window.scrollTo({ top: key === 'Home' ? 0 : document.documentElement.scrollHeight });
726
+ return;
727
+ default:
728
+ if (key.length === 1 && text) insertText(el, key);
729
+ }
730
+ }
731
+
732
+ function deleteChar(el, dir) {
733
+ if (el.value == null) return;
734
+ let s = el.selectionStart ?? el.value.length;
735
+ let e = el.selectionEnd ?? s;
736
+ if (s === e) (dir < 0 ? (s = Math.max(0, s - 1)) : (e = Math.min(el.value.length, e + 1)));
737
+ setNativeValue(el, el.value.slice(0, s) + el.value.slice(e));
738
+ el.setSelectionRange?.(s, s);
739
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: dir < 0 ? 'deleteContentBackward' : 'deleteContentForward' }));
740
+ }
741
+
742
+ function stepInput(el, dir) {
743
+ try { dir > 0 ? el.stepUp() : el.stepDown(); } catch { return; }
744
+ el.dispatchEvent(new Event('input', { bubbles: true }));
745
+ el.dispatchEvent(new Event('change', { bubbles: true }));
746
+ }