openzoo 0.48.83 → 0.48.84

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 (2) hide show
  1. package/lib/grokui.mjs +151 -0
  2. package/package.json +1 -1
package/lib/grokui.mjs CHANGED
@@ -2757,8 +2757,25 @@ const APP_HTML = `<!doctype html>
2757
2757
  font-size: 12px; }
2758
2758
  #composeFoot kbd { margin-right: 4px; }
2759
2759
  .mention { background: #3a3a3c; border-radius: 10px; padding: 1px 8px; font-size: 0.92em; }
2760
+ /* Sidebar names and header titles are readable text — they must be
2761
+ selectable so select-to-copy works there, not only in bubbles. */
2762
+ .tname, .tprev, .pname, .hname, .hdir, .hdr, .wbal, .wnote, .wsub, #chatHeader {
2763
+ -webkit-user-select: text; user-select: text; }
2764
+ /* SELECT-TO-COPY. A blink, not a dialog: selection landed on the clipboard. */
2765
+ #copiedToast {
2766
+ position: fixed; z-index: 4000; pointer-events: none;
2767
+ padding: 7px 14px; border-radius: 999px;
2768
+ background: #b8f240; color: #0b0b0d;
2769
+ font: 700 12px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2770
+ letter-spacing: .06em;
2771
+ box-shadow: 0 8px 24px rgba(0,0,0,.5);
2772
+ opacity: 0; transform: translate(-50%, -6px);
2773
+ transition: opacity .14s ease, transform .14s ease;
2774
+ }
2775
+ #copiedToast.show { opacity: 1; transform: translate(-50%, 0); }
2760
2776
  </style></head>
2761
2777
  <body>
2778
+ <div id="copiedToast" role="status" aria-live="polite">copied</div>
2762
2779
  <div id="dragbar"></div>
2763
2780
  <div id="sidebar">
2764
2781
  <div id="sideTop">
@@ -3338,6 +3355,140 @@ const APP_HTML = `<!doctype html>
3338
3355
  return b;
3339
3356
  }
3340
3357
 
3358
+ /* SELECT → CLIPBOARD. A finished selection copies itself. No ⌘C, no hunting
3359
+ for a 10px button. Empty click / caret-only is a no-op so we do not spam
3360
+ "copied". The highlight stays put: copy must not steal focus or collapse
3361
+ the range. */
3362
+ function selectedText() {
3363
+ const el = document.activeElement;
3364
+ if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
3365
+ if (el.type === 'password') return '';
3366
+ const a = el.selectionStart;
3367
+ const b = el.selectionEnd;
3368
+ if (typeof a === 'number' && typeof b === 'number' && b > a) {
3369
+ return String(el.value || '').slice(a, b);
3370
+ }
3371
+ return '';
3372
+ }
3373
+ const sel = window.getSelection();
3374
+ return sel ? sel.toString() : '';
3375
+ }
3376
+ function selectedRect() {
3377
+ const el = document.activeElement;
3378
+ if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') && el.type !== 'password') {
3379
+ const a = el.selectionStart;
3380
+ const b = el.selectionEnd;
3381
+ if (typeof a === 'number' && typeof b === 'number' && b > a) return el.getBoundingClientRect();
3382
+ }
3383
+ const sel = window.getSelection();
3384
+ if (!sel || !sel.rangeCount) return null;
3385
+ const r = sel.getRangeAt(0).getBoundingClientRect();
3386
+ if (!r || (r.width === 0 && r.height === 0)) return null;
3387
+ return r;
3388
+ }
3389
+ function snapshotSelection() {
3390
+ const el = document.activeElement;
3391
+ if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
3392
+ return { kind: 'field', el: el, start: el.selectionStart, end: el.selectionEnd };
3393
+ }
3394
+ const sel = window.getSelection();
3395
+ if (!sel || !sel.rangeCount) return { kind: 'none' };
3396
+ const ranges = [];
3397
+ for (let i = 0; i < sel.rangeCount; i++) ranges.push(sel.getRangeAt(i).cloneRange());
3398
+ return { kind: 'dom', ranges: ranges };
3399
+ }
3400
+ function restoreSelection(snap) {
3401
+ if (!snap) return;
3402
+ if (snap.kind === 'field' && snap.el && document.contains(snap.el)) {
3403
+ try {
3404
+ snap.el.focus({ preventScroll: true });
3405
+ snap.el.setSelectionRange(snap.start, snap.end);
3406
+ } catch (e) {}
3407
+ return;
3408
+ }
3409
+ if (snap.kind === 'dom') {
3410
+ const sel = window.getSelection();
3411
+ sel.removeAllRanges();
3412
+ for (let i = 0; i < snap.ranges.length; i++) {
3413
+ try { sel.addRange(snap.ranges[i]); } catch (e) {}
3414
+ }
3415
+ }
3416
+ }
3417
+ let copiedToastTimer = 0;
3418
+ function showCopiedToast(rect) {
3419
+ const el = document.getElementById('copiedToast');
3420
+ if (!el) return;
3421
+ let x = window.innerWidth / 2;
3422
+ let y = 40;
3423
+ if (rect && (rect.width || rect.height)) {
3424
+ x = rect.left + rect.width / 2;
3425
+ y = rect.top - 38;
3426
+ if (y < 8) y = rect.bottom + 10;
3427
+ }
3428
+ if (x < 48) x = 48;
3429
+ if (x > window.innerWidth - 48) x = window.innerWidth - 48;
3430
+ if (y < 8) y = 8;
3431
+ if (y > window.innerHeight - 36) y = window.innerHeight - 36;
3432
+ el.style.left = x + 'px';
3433
+ el.style.top = y + 'px';
3434
+ el.classList.add('show');
3435
+ clearTimeout(copiedToastTimer);
3436
+ copiedToastTimer = setTimeout(function () { el.classList.remove('show'); }, 1200);
3437
+ }
3438
+ let selectPointerDown = false;
3439
+ let selectShiftHeld = false;
3440
+ let selectCopying = false;
3441
+ let selectTimer = 0;
3442
+ let selectLastText = '';
3443
+ let selectLastAt = 0;
3444
+ async function copySettledSelection() {
3445
+ if (selectCopying) return;
3446
+ const text = selectedText();
3447
+ if (!text) return;
3448
+ const now = Date.now();
3449
+ if (text === selectLastText && (now - selectLastAt) < 500) return;
3450
+ selectLastText = text;
3451
+ selectLastAt = now;
3452
+ const rect = selectedRect();
3453
+ const snap = snapshotSelection();
3454
+ selectCopying = true;
3455
+ let ok = false;
3456
+ try { ok = await copyText(text); }
3457
+ finally { restoreSelection(snap); selectCopying = false; }
3458
+ if (ok) showCopiedToast(rect);
3459
+ }
3460
+ function scheduleCopySelection() {
3461
+ if (selectPointerDown || selectCopying) return;
3462
+ clearTimeout(selectTimer);
3463
+ selectTimer = setTimeout(copySettledSelection, 40);
3464
+ }
3465
+ document.addEventListener('pointerdown', function (e) {
3466
+ if (e.button !== undefined && e.button !== 0) return;
3467
+ selectPointerDown = true;
3468
+ });
3469
+ document.addEventListener('pointerup', function (e) {
3470
+ if (e.button !== undefined && e.button !== 0) return;
3471
+ selectPointerDown = false;
3472
+ scheduleCopySelection();
3473
+ });
3474
+ document.addEventListener('pointercancel', function () { selectPointerDown = false; });
3475
+ document.addEventListener('keydown', function (e) {
3476
+ if (e.key === 'Shift') selectShiftHeld = true;
3477
+ });
3478
+ document.addEventListener('keyup', function (e) {
3479
+ if (e.key === 'Shift') selectShiftHeld = false;
3480
+ const withMod = e.metaKey || e.ctrlKey;
3481
+ const selectish = e.key === 'Shift'
3482
+ || e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown'
3483
+ || e.key === 'Home' || e.key === 'End' || e.key === 'PageUp' || e.key === 'PageDown'
3484
+ || ((e.key === 'a' || e.key === 'A') && withMod);
3485
+ if (selectish) scheduleCopySelection();
3486
+ });
3487
+ document.addEventListener('selectionchange', function () {
3488
+ if (selectPointerDown || selectCopying || selectShiftHeld) return;
3489
+ scheduleCopySelection();
3490
+ });
3491
+
3341
3492
  function renderMentions(text) {
3342
3493
  const fences = [];
3343
3494
  const src = String(text).replace(/\`\`\`([\\w-]*)\\n?([\\s\\S]*?)\`\`\`/g, (m, lang, code) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.83",
3
+ "version": "0.48.84",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",