@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,425 @@
1
+ /**
2
+ * UIManager — interfaz dinámica que el agente inyecta en la página anfitriona.
3
+ *
4
+ * Todo vive dentro de UN host `<div data-ots-root="overlay">` con Shadow DOM propio,
5
+ * añadido a <body>: aparece "dentro" de la página (encima del contenido), pero ni el
6
+ * CSS del sitio rompe nuestros estilos ni los nuestros rompen el sitio.
7
+ *
8
+ * Componentes:
9
+ * toast({message, type, duration})
10
+ * modal({title, content|template+data, actions, size}) → Promise<valor del botón | null>
11
+ * sidebar({title, content|template+data, side}) / closeSidebar()
12
+ * confirm({title, message, confirmText, cancelText}) → Promise<boolean>
13
+ * highlight(element, {message, duration}) → foco visual sobre un elemento real
14
+ * card({title, content, facts, target, actions}) → tarjeta de información junto a un elemento
15
+ * registerTemplate(name, (data, {escape}) => html) → HTML rico de CONFIANZA (del dev)
16
+ *
17
+ * El contenido que genera el LLM pasa por renderMarkdown (sin HTML crudo). Las plantillas
18
+ * son código del desarrollador: reciben datos del LLM y deben escaparlos con `escape`.
19
+ */
20
+
21
+ import { escapeHtml, renderMarkdown } from './markdown.js';
22
+ import { translator } from '../i18n/index.js';
23
+ import '../i18n/messages/widget.js';
24
+
25
+ /**
26
+ * Lleva un elemento al centro de la pantalla. Suave salvo si el usuario prefiere
27
+ * menos movimiento o la pestaña está oculta (ahí el scroll suave no llega a ejecutarse).
28
+ */
29
+ export function revealElement(el) {
30
+ const instant = document.hidden || matchMedia('(prefers-reduced-motion: reduce)').matches;
31
+ el.scrollIntoView({ behavior: instant ? 'instant' : 'smooth', block: 'center' });
32
+ }
33
+
34
+ export class UIManager {
35
+ /**
36
+ * @param {object} [o]
37
+ * @param {object} [o.bus]
38
+ * @param {object} [o.theme]
39
+ * @param {(key: string, vars?: object) => string} [o.t] traductor (por defecto, el idioma global)
40
+ */
41
+ constructor({ bus, theme = {}, t = translator() } = {}) {
42
+ this.bus = bus;
43
+ this.theme = theme;
44
+ this.t = t;
45
+ this.templates = new Map();
46
+ this._host = null;
47
+ this._root = null;
48
+ this._seq = 0;
49
+ this._highlights = new Set();
50
+ this._card = null;
51
+ /** Dónde abrir una tarjeta sin elemento: el widget pone aquí el avatar compañero. */
52
+ this.cardAnchor = () => null;
53
+ }
54
+
55
+ /** Color principal en caliente (identidad nueva). */
56
+ setPrimary(color) {
57
+ this.theme = { ...this.theme, primary: color };
58
+ this._host?.style.setProperty('--ots-primary', color);
59
+ }
60
+
61
+ mount() {
62
+ if (this._host) return;
63
+ this._host = document.createElement('div');
64
+ this._host.setAttribute('data-ots-root', 'overlay');
65
+ // El host no ocupa espacio ni captura clics; solo sus hijos lo hacen.
66
+ this._host.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483646;';
67
+ if (this.theme.primary) this._host.style.setProperty('--ots-primary', this.theme.primary);
68
+ this._root = this._host.attachShadow({ mode: 'open' });
69
+ this._root.innerHTML = `<style>${overlayCss(this.theme)}</style>
70
+ <div class="toasts" part="toasts" aria-live="polite"></div>
71
+ <div class="layer"></div>`;
72
+ document.body.appendChild(this._host);
73
+ }
74
+
75
+ destroy() {
76
+ this._highlights.forEach((h) => h.remove());
77
+ this._host?.remove();
78
+ this._host = this._root = null;
79
+ }
80
+
81
+ registerTemplate(name, render) {
82
+ this.templates.set(name, render);
83
+ }
84
+
85
+ /** Renderiza contenido: plantilla del dev (confiable) o markdown del LLM (escapado). */
86
+ renderContent({ content = '', template = null, data = null } = {}) {
87
+ if (template) {
88
+ const fn = this.templates.get(template);
89
+ if (!fn) return `<p>${escapeHtml(content || '')}</p>`;
90
+ try {
91
+ return fn(data || {}, { escape: escapeHtml, markdown: renderMarkdown });
92
+ } catch (err) {
93
+ console.error('[7ots] plantilla falló', template, err);
94
+ return `<p>${escapeHtml(content || '')}</p>`;
95
+ }
96
+ }
97
+ return renderMarkdown(content);
98
+ }
99
+
100
+ // ───────────────────────────── Toast ─────────────────────────────
101
+
102
+ toast({ message, type = 'info', duration = 5000 } = {}) {
103
+ this.mount();
104
+ const el = document.createElement('div');
105
+ el.className = `toast ${['info', 'success', 'warning', 'error'].includes(type) ? type : 'info'}`;
106
+ el.setAttribute('role', type === 'error' ? 'alert' : 'status');
107
+ el.innerHTML = `<div class="toast-body">${renderMarkdown(message)}</div><button class="x" aria-label="${escapeHtml(this.t('widget.dialog.close'))}">×</button>`;
108
+ const close = () => {
109
+ el.classList.add('out');
110
+ setTimeout(() => el.remove(), 250);
111
+ };
112
+ el.querySelector('.x').onclick = close;
113
+ this._root.querySelector('.toasts').appendChild(el);
114
+ if (duration > 0) setTimeout(close, Math.min(duration, 30000));
115
+ return close;
116
+ }
117
+
118
+ // ───────────────────────────── Modal ─────────────────────────────
119
+
120
+ /**
121
+ * @returns {Promise<string|null>} valor del botón pulsado, o null si se cerró.
122
+ */
123
+ modal({ title = '', content = '', template = null, data = null, actions = [], size = 'md', dismissible = true } = {}) {
124
+ this.mount();
125
+ return new Promise((resolve) => {
126
+ const id = `m${++this._seq}`;
127
+ // Si la página tiene abierto un <dialog> modal, todo lo demás es inert: el nuestro se abre
128
+ // también como modal nativo (capa superior, encima del suyo) para que se pueda pulsar.
129
+ const overPageModal = !!document.querySelector('dialog:modal');
130
+ const wrap = document.createElement(overPageModal ? 'dialog' : 'div');
131
+ wrap.className = 'backdrop';
132
+ wrap.innerHTML = `
133
+ <div class="modal ${size}" role="dialog" aria-modal="true" aria-labelledby="${id}-t">
134
+ <header><h2 id="${id}-t">${escapeHtml(title)}</h2>${dismissible ? `<button class="x" aria-label="${escapeHtml(this.t('widget.dialog.close'))}">×</button>` : ''}</header>
135
+ <div class="content">${this.renderContent({ content, template, data })}</div>
136
+ ${actions.length ? `<footer>${actions.map((a, i) => `<button class="btn ${a.primary ? 'primary' : ''}" data-i="${i}">${escapeHtml(a.label)}</button>`).join('')}</footer>` : ''}
137
+ </div>`;
138
+ const prevFocus = document.activeElement;
139
+ const done = (value) => {
140
+ wrap.classList.add('out');
141
+ document.removeEventListener('keydown', onKey, true);
142
+ wrap.close?.(); // el modal nativo se cierra ya: si no, la página sigue inert durante la animación
143
+ setTimeout(() => wrap.remove(), 200);
144
+ prevFocus?.focus?.();
145
+ resolve(value);
146
+ };
147
+ const onKey = (e) => { if (e.key === 'Escape' && dismissible) { e.stopPropagation(); done(null); } };
148
+ document.addEventListener('keydown', onKey, true);
149
+ wrap.addEventListener('click', (e) => {
150
+ if (e.target === wrap && dismissible) done(null);
151
+ if (e.target.closest('.x')) done(null);
152
+ const b = e.target.closest('button[data-i]');
153
+ if (b) done(actions[Number(b.dataset.i)].value ?? actions[Number(b.dataset.i)].label);
154
+ });
155
+ this._root.querySelector('.layer').appendChild(wrap);
156
+ if (overPageModal) {
157
+ wrap.addEventListener('cancel', (e) => e.preventDefault()); // Esc lo gestiona onKey
158
+ wrap.showModal();
159
+ }
160
+ (wrap.querySelector('.btn.primary') || wrap.querySelector('button'))?.focus();
161
+ });
162
+ }
163
+
164
+ confirm({ title = this.t('widget.dialog.confirm'), message = '', confirmText = this.t('widget.dialog.ok'), cancelText = this.t('widget.dialog.cancel') } = {}) {
165
+ return this.modal({
166
+ title,
167
+ content: message,
168
+ size: 'sm',
169
+ actions: [
170
+ { label: cancelText, value: 'no' },
171
+ { label: confirmText, value: 'yes', primary: true },
172
+ ],
173
+ }).then((v) => v === 'yes');
174
+ }
175
+
176
+ // ───────────────────────────── Sidebar ─────────────────────────────
177
+
178
+ sidebar({ title = '', content = '', template = null, data = null, side = 'right' } = {}) {
179
+ this.mount();
180
+ this.closeSidebar();
181
+ const el = document.createElement('aside');
182
+ el.className = `sidebar ${side === 'left' ? 'left' : 'right'}`;
183
+ el.setAttribute('role', 'complementary');
184
+ el.innerHTML = `<header><h2>${escapeHtml(title)}</h2><button class="x" aria-label="${escapeHtml(this.t('widget.dialog.close'))}">×</button></header>
185
+ <div class="content">${this.renderContent({ content, template, data })}</div>`;
186
+ el.querySelector('.x').onclick = () => this.closeSidebar();
187
+ this._root.querySelector('.layer').appendChild(el);
188
+ requestAnimationFrame(() => el.classList.add('in'));
189
+ return () => this.closeSidebar();
190
+ }
191
+
192
+ closeSidebar() {
193
+ const el = this._root?.querySelector('.sidebar');
194
+ if (!el) return;
195
+ el.classList.remove('in');
196
+ setTimeout(() => el.remove(), 250);
197
+ }
198
+
199
+ closeAll() {
200
+ this._root?.querySelectorAll('.backdrop').forEach((b) => b.querySelector('.x')?.click() ?? b.remove());
201
+ this.closeSidebar();
202
+ this.closeCard();
203
+ this.clearHighlights();
204
+ }
205
+
206
+ // ───────────────────────────── Tarjeta ─────────────────────────────
207
+
208
+ /**
209
+ * Tarjeta de información que "despliega" el agente junto a un elemento de la página (o junto
210
+ * al avatar): título, texto en markdown, datos clave y botones de pregunta rápida.
211
+ * Solo hay una a la vez; sigue al elemento al hacer scroll.
212
+ * @param {object} o
213
+ * @param {Element|null} [o.target]
214
+ * @param {{label:string, value:string}[]} [o.facts]
215
+ * @param {{label:string, onClick:()=>void}[]} [o.actions]
216
+ * @param {number} [o.duration] ms (0 = hasta que se cierre)
217
+ */
218
+ card({ title = '', content = '', facts = [], target = null, actions = [], duration = 0, tone = 'info' } = {}) {
219
+ this.mount();
220
+ this.closeCard();
221
+ const el = document.createElement('div');
222
+ el.className = `card ${['info', 'tip', 'warning', 'success'].includes(tone) ? tone : 'info'}`;
223
+ el.setAttribute('role', 'dialog');
224
+ el.setAttribute('aria-label', title || this.t('widget.dialog.info'));
225
+ el.innerHTML = `
226
+ <header><h3>${escapeHtml(title)}</h3><button class="x" aria-label="${escapeHtml(this.t('widget.dialog.close'))}">×</button></header>
227
+ ${content ? `<div class="card-body">${renderMarkdown(content)}</div>` : ''}
228
+ ${facts.length ? `<dl>${facts.slice(0, 8).map((f) => `<div><dt>${escapeHtml(f.label)}</dt><dd>${escapeHtml(f.value)}</dd></div>`).join('')}</dl>` : ''}
229
+ ${actions.length ? `<footer>${actions.slice(0, 3).map((a, i) => `<button class="chip" data-i="${i}">${escapeHtml(a.label)}</button>`).join('')}</footer>` : ''}
230
+ <span class="card-tail" aria-hidden="true"></span>`;
231
+ const layer = this._root.querySelector('.layer');
232
+ layer.appendChild(el);
233
+
234
+ let raf = 0;
235
+ let closed = false;
236
+ const close = () => {
237
+ if (closed) return;
238
+ closed = true;
239
+ cancelAnimationFrame(raf);
240
+ el.classList.add('out');
241
+ setTimeout(() => el.remove(), 200);
242
+ if (this._card === handle) this._card = null;
243
+ };
244
+ const handle = { close };
245
+ this._card = handle;
246
+ el.querySelector('.x').onclick = close;
247
+ el.addEventListener('click', (e) => {
248
+ const b = e.target.closest('button[data-i]');
249
+ if (!b) return;
250
+ close();
251
+ actions[Number(b.dataset.i)]?.onClick?.();
252
+ });
253
+
254
+ const tail = el.querySelector('.card-tail');
255
+ const place = () => {
256
+ if (closed) return;
257
+ const anchor = target?.isConnected ? target : this.cardAnchor();
258
+ const w = el.offsetWidth;
259
+ const h = el.offsetHeight;
260
+ let x;
261
+ let y;
262
+ let side = 'none';
263
+ if (anchor) {
264
+ const r = anchor.getBoundingClientRect();
265
+ // Preferencia: a la derecha, a la izquierda, debajo, encima.
266
+ if (r.right + 16 + w < innerWidth - 8) [x, y, side] = [r.right + 16, r.top + r.height / 2 - h / 2, 'left'];
267
+ else if (r.left - 16 - w > 8) [x, y, side] = [r.left - 16 - w, r.top + r.height / 2 - h / 2, 'right'];
268
+ else if (r.bottom + 16 + h < innerHeight - 8) [x, y, side] = [r.left + r.width / 2 - w / 2, r.bottom + 16, 'top'];
269
+ else [x, y, side] = [r.left + r.width / 2 - w / 2, r.top - 16 - h, 'bottom'];
270
+ } else {
271
+ [x, y] = [innerWidth - w - 24, innerHeight - h - 96];
272
+ }
273
+ x = Math.min(Math.max(8, x), innerWidth - w - 8);
274
+ y = Math.min(Math.max(8, y), innerHeight - h - 8);
275
+ el.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
276
+ tail.dataset.side = side;
277
+ if (anchor && side !== 'none') {
278
+ const r = anchor.getBoundingClientRect();
279
+ if (side === 'left' || side === 'right') tail.style.top = `${Math.min(Math.max(14, r.top + r.height / 2 - y), h - 14)}px`;
280
+ else tail.style.left = `${Math.min(Math.max(14, r.left + r.width / 2 - x), w - 14)}px`;
281
+ }
282
+ raf = requestAnimationFrame(place);
283
+ };
284
+ place();
285
+ if (duration > 0) setTimeout(close, Math.min(duration, 120000));
286
+ return close;
287
+ }
288
+
289
+ closeCard() {
290
+ this._card?.close();
291
+ }
292
+
293
+ // ───────────────────────────── Highlight ─────────────────────────────
294
+
295
+ /**
296
+ * Dibuja un marco pulsante sobre un elemento REAL de la página (sin modificarlo)
297
+ * y, opcionalmente, un globo con un mensaje. Sigue al elemento si hay scroll.
298
+ */
299
+ highlight(target, { message = '', duration = 6000, scroll = true } = {}) {
300
+ if (!target) return () => {};
301
+ this.mount();
302
+ if (scroll) revealElement(target);
303
+ const box = document.createElement('div');
304
+ box.className = 'spot';
305
+ const tip = message ? document.createElement('div') : null;
306
+ if (tip) {
307
+ tip.className = 'tip';
308
+ tip.innerHTML = renderMarkdown(message);
309
+ }
310
+ const layer = this._root.querySelector('.layer');
311
+ layer.appendChild(box);
312
+ tip && layer.appendChild(tip);
313
+
314
+ let raf = 0;
315
+ const place = () => {
316
+ if (!target.isConnected) return stop();
317
+ const r = target.getBoundingClientRect();
318
+ const pad = 6;
319
+ Object.assign(box.style, { top: `${r.top - pad}px`, left: `${r.left - pad}px`, width: `${r.width + pad * 2}px`, height: `${r.height + pad * 2}px` });
320
+ if (tip) {
321
+ const below = r.bottom + 12 + tip.offsetHeight < innerHeight;
322
+ tip.style.top = `${below ? r.bottom + 12 : Math.max(8, r.top - tip.offsetHeight - 12)}px`;
323
+ tip.style.left = `${Math.min(Math.max(8, r.left), innerWidth - tip.offsetWidth - 8)}px`;
324
+ }
325
+ raf = requestAnimationFrame(place);
326
+ };
327
+ const handle = { remove: () => stop() };
328
+ const stop = () => {
329
+ cancelAnimationFrame(raf);
330
+ box.remove();
331
+ tip?.remove();
332
+ this._highlights.delete(handle);
333
+ };
334
+ this._highlights.add(handle);
335
+ place();
336
+ if (duration > 0) setTimeout(stop, duration);
337
+ return stop;
338
+ }
339
+
340
+ clearHighlights() {
341
+ [...this._highlights].forEach((h) => h.remove());
342
+ }
343
+ }
344
+
345
+ function overlayCss(theme) {
346
+ const primary = theme.primary || '#5b5bf0';
347
+ const radius = theme.radius || '14px';
348
+ const font = theme.font || 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
349
+ return `
350
+ :host { all: initial; }
351
+ * { box-sizing: border-box; }
352
+ .toasts, .layer { font-family: ${font}; }
353
+ .toasts, .layer, .toast, .modal, .sidebar, .tip, .card {
354
+ --bg: #ffffff; --fg: #1d1d27; --muted: #6b6b7b; --line: #e6e6ef; --primary: var(--ots-primary, ${primary});
355
+ }
356
+ @media (prefers-color-scheme: dark) {
357
+ .toasts, .layer, .toast, .modal, .sidebar, .tip, .card { --bg: #1c1c24; --fg: #f1f1f5; --muted: #a3a3b3; --line: #33333f; }
358
+ }
359
+ .toasts { position: fixed; top: 16px; right: 16px; display: flex; flex-direction: column; gap: 10px; max-width: min(380px, calc(100vw - 32px)); }
360
+ .toast { pointer-events: auto; display: flex; gap: 10px; align-items: flex-start; background: var(--bg); color: var(--fg);
361
+ border: 1px solid var(--line); border-left: 4px solid var(--primary); border-radius: ${radius}; padding: 12px 12px 12px 14px;
362
+ box-shadow: 0 10px 30px rgba(0,0,0,.15); font-size: 14px; line-height: 1.45; animation: in .25s ease; }
363
+ .toast.success { border-left-color: #1f9d55; } .toast.warning { border-left-color: #d98e04; } .toast.error { border-left-color: #d64545; }
364
+ .toast-body { flex: 1; } .toast-body p { margin: 0; }
365
+ .out { opacity: 0; transform: translateY(-6px); transition: all .2s ease; }
366
+ .x { all: unset; cursor: pointer; font-size: 20px; line-height: 1; color: var(--muted); padding: 2px 6px; border-radius: 6px; }
367
+ .x:hover { background: rgba(127,127,127,.15); }
368
+ dialog.backdrop { margin: 0; border: 0; width: 100%; height: 100%; max-width: none; max-height: none; color: inherit; }
369
+ dialog.backdrop::backdrop { background: transparent; }
370
+ .backdrop { position: fixed; inset: 0; background: rgba(10,10,20,.45); display: grid; place-items: center; pointer-events: auto; padding: 16px; animation: fade .2s ease; }
371
+ .modal { background: var(--bg); color: var(--fg); border-radius: ${radius}; width: min(560px, 100%); max-height: calc(100vh - 32px);
372
+ display: flex; flex-direction: column; box-shadow: 0 24px 60px rgba(0,0,0,.3); animation: in .22s ease; }
373
+ .modal.sm { width: min(420px, 100%); } .modal.lg { width: min(820px, 100%); }
374
+ header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px 8px; }
375
+ h2 { margin: 0; font-size: 18px; }
376
+ .content { padding: 4px 18px 16px; overflow: auto; font-size: 15px; line-height: 1.55; }
377
+ .content h2, .content h3, .content h4 { margin: 14px 0 6px; font-size: 15px; }
378
+ .content a { color: var(--primary); }
379
+ .content code { background: rgba(127,127,127,.15); padding: 1px 5px; border-radius: 5px; }
380
+ footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 18px 16px; border-top: 1px solid var(--line); flex-wrap: wrap; }
381
+ .btn { all: unset; cursor: pointer; padding: 9px 16px; border-radius: 10px; border: 1px solid var(--line); font-size: 14px; font-weight: 500; }
382
+ .btn:hover { background: rgba(127,127,127,.1); }
383
+ .btn.primary { background: var(--primary); color: #fff; border-color: transparent; }
384
+ .btn.primary:hover { filter: brightness(1.08); }
385
+ .btn:focus-visible, .x:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
386
+ .sidebar { position: fixed; top: 0; bottom: 0; width: min(380px, 92vw); background: var(--bg); color: var(--fg); pointer-events: auto;
387
+ box-shadow: 0 0 40px rgba(0,0,0,.2); display: flex; flex-direction: column; transition: transform .25s ease; }
388
+ .sidebar.right { right: 0; transform: translateX(105%); } .sidebar.left { left: 0; transform: translateX(-105%); }
389
+ .sidebar.in { transform: none; }
390
+ .spot { position: fixed; border: 3px solid var(--primary); border-radius: 10px; pointer-events: none;
391
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 25%, transparent), 0 0 0 9999px rgba(10,10,20,.18);
392
+ animation: pulse 1.4s ease-in-out infinite; }
393
+ .tip { position: fixed; max-width: 300px; background: var(--primary); color: #fff; padding: 10px 12px; border-radius: 10px;
394
+ font-size: 14px; line-height: 1.4; box-shadow: 0 8px 24px rgba(0,0,0,.2); pointer-events: none; }
395
+ .tip p { margin: 0; } .tip a { color: #fff; }
396
+ .card { position: fixed; left: 0; top: 0; width: min(320px, calc(100vw - 16px)); background: var(--bg); color: var(--fg);
397
+ border: 1px solid var(--line); border-top: 4px solid var(--primary); border-radius: ${radius}; pointer-events: auto;
398
+ box-shadow: 0 16px 44px rgba(0,0,0,.22); font-size: 14px; line-height: 1.45; animation: pop .28s cubic-bezier(.2,1.4,.4,1); }
399
+ .card.tip { border-top-color: #1f9d55; } .card.warning { border-top-color: #d98e04; } .card.success { border-top-color: #1f9d55; }
400
+ .card header { padding: 12px 12px 4px 14px; }
401
+ .card h3 { margin: 0; font-size: 15px; }
402
+ .card-body { padding: 0 14px 8px; } .card-body p { margin: 0 0 6px; } .card-body ul { margin: 0 0 6px; padding-left: 18px; }
403
+ .card dl { margin: 0; padding: 4px 14px 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
404
+ .card dl div { background: color-mix(in srgb, var(--primary) 8%, transparent); border-radius: 8px; padding: 6px 8px; }
405
+ .card dt { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
406
+ .card dd { margin: 2px 0 0; font-weight: 600; }
407
+ .card footer { border: 0; padding: 4px 14px 12px; justify-content: flex-start; gap: 6px; }
408
+ .chip { all: unset; cursor: pointer; font-size: 13px; padding: 6px 10px; border-radius: 999px; color: var(--primary);
409
+ border: 1px solid color-mix(in srgb, var(--primary) 45%, transparent); }
410
+ .chip:hover { background: color-mix(in srgb, var(--primary) 10%, transparent); }
411
+ .chip:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
412
+ .card-tail { position: absolute; width: 12px; height: 12px; background: var(--bg); border: 1px solid var(--line); transform: rotate(45deg); }
413
+ .card-tail[data-side="none"] { display: none; }
414
+ .card-tail[data-side="left"] { left: -7px; margin-top: -6px; border-top-color: transparent; border-right-color: transparent; }
415
+ .card-tail[data-side="right"] { right: -7px; margin-top: -6px; border-bottom-color: transparent; border-left-color: transparent; }
416
+ .card-tail[data-side="top"] { top: -8px; margin-left: -6px; border-bottom-color: transparent; border-right-color: transparent; }
417
+ .card-tail[data-side="bottom"] { bottom: -7px; margin-left: -6px; border-top-color: transparent; border-left-color: transparent; }
418
+ .card.out { opacity: 0; transition: opacity .18s ease; }
419
+ @keyframes pop { from { opacity: 0; scale: .85; } }
420
+ @keyframes in { from { opacity: 0; transform: translateY(8px) scale(.98); } }
421
+ @keyframes fade { from { opacity: 0; } }
422
+ @keyframes pulse { 50% { box-shadow: 0 0 0 8px color-mix(in srgb, var(--primary) 15%, transparent), 0 0 0 9999px rgba(10,10,20,.18); } }
423
+ @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
424
+ `;
425
+ }