@qzsy/dsh-bridge 3.3.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.
package/client/client.js CHANGED
@@ -220,27 +220,63 @@ var MOBILE_MAX_WIDTH = 767;
220
220
  var MOBILE_DRAWER_CSS_ID = "qzsy-mobile-drawer-styles";
221
221
  var MOBILE_UI_FLAG = "data-qzsy-mobile-drawer";
222
222
  var OPEN_CLASS = "qzsy-drawer-open";
223
+ var DRAGGING_CLASS = "qzsy-drawer-dragging";
224
+ var HEADER_CLASS = "qzsy-mobile-header";
223
225
  var MENU_CLASS = "qzsy-mobile-menu-btn";
224
226
  var BACKDROP_CLASS = "qzsy-mobile-backdrop";
225
227
  var STYLE_PLUGIN = "@qzsy/dsh-bridge";
228
+ var EDGE_START_ZONE = 28;
229
+ var DRAG_THRESHOLD = 8;
226
230
  var MOBILE_DRAWER_CSS = `
227
- .qzsy-mobile-menu-btn,
228
- .qzsy-mobile-backdrop {
231
+ :root {
232
+ --qzsy-safe-top: env(safe-area-inset-top, 0px);
233
+ --qzsy-safe-bottom: env(safe-area-inset-bottom, 0px);
234
+ --qzsy-safe-left: env(safe-area-inset-left, 0px);
235
+ --qzsy-safe-right: env(safe-area-inset-right, 0px);
236
+ }
237
+
238
+ .qzsy-mobile-header,
239
+ .qzsy-mobile-backdrop,
240
+ .qzsy-model-sheet-backdrop {
229
241
  display: none;
230
242
  }
231
243
 
244
+ /* \u83DC\u5355\u6309\u94AE\u5BB9\u5668\uFF1A\u59CB\u7EC8\u5728\u5BBF\u4E3B\u4E4B\u4E0A\uFF1B\u5BB9\u5668\u81EA\u8EAB\u4E0D\u62E6\u622A\u70B9\u51FB */
245
+ .qzsy-mobile-header {
246
+ position: fixed;
247
+ top: 0;
248
+ left: 0;
249
+ right: 0;
250
+ z-index: 65;
251
+ box-sizing: border-box;
252
+ align-items: flex-end;
253
+ padding-top: 10px;
254
+ padding-top: calc(10px + var(--qzsy-safe-top, 0px));
255
+ padding-right: 10px;
256
+ padding-right: calc(10px + var(--qzsy-safe-right, 0px));
257
+ padding-bottom: 2px;
258
+ padding-left: 10px;
259
+ padding-left: calc(10px + var(--qzsy-safe-left, 0px));
260
+ pointer-events: none;
261
+ }
262
+
232
263
  @media (max-width: 767px) {
233
- /* 1. \u5DE6\u4FA7\u680F\u8131\u79BB grid\uFF0C\u5185\u5BB9\u5360\u6EE1\uFF1B\u9876\u90E8\u7559\u51FA 52px \u7ED9\u83DC\u5355\u6309\u94AE */
264
+ .qzsy-mobile-header { display: flex; }
265
+
266
+ /* 1. \u5DE6\u4FA7\u680F\u8131\u79BB grid\uFF0C\u5185\u5BB9\u5360\u6EE1\uFF1B\u9876\u90E8\u7ED9\u83DC\u5355\u6309\u94AE\u7559\u51FA\u5B89\u5168\u533A\u9AD8\u5EA6 */
234
267
  [data-slot="root"] > div[class*="_frame"] {
235
268
  grid-template-columns: 0px minmax(0px, 1fr) minmax(0px, 0px) !important;
236
- padding-top: 52px !important;
237
269
  box-sizing: border-box !important;
270
+ padding-top: 52px !important;
271
+ padding-top: calc(52px + var(--qzsy-safe-top, 0px)) !important;
272
+ padding-bottom: 0 !important;
273
+ padding-bottom: var(--qzsy-safe-bottom, 0px) !important;
238
274
  }
239
275
  div[class*="_centerCol"] { grid-column: 2 !important; }
240
276
  div[class*="_rightbarCol"] { grid-column: 3 !important; }
241
277
  div[class*="_overlayLayer"] { grid-column: 1 / -1 !important; }
242
278
 
243
- /* 2. \u539F\u751F\u4FA7\u680F\u6539\u4E3A fixed \u62BD\u5C49\uFF08\u9ED8\u8BA4\u85CF\u5728\u5C4F\u5E55\u5DE6\u4FA7\uFF09 */
279
+ /* 2. \u539F\u751F\u4FA7\u680F\u6539\u4E3A fixed \u62BD\u5C49\uFF08\u9ED8\u8BA4\u85CF\u5728\u5C4F\u5E55\u5DE6\u4FA7\uFF1B\u5185\u90E8\u907F\u5F00\u5B89\u5168\u533A\uFF09 */
244
280
  div[class*="_sidebarCol"] {
245
281
  position: fixed !important;
246
282
  top: 0 !important;
@@ -250,16 +286,26 @@ var MOBILE_DRAWER_CSS = `
250
286
  max-width: 84vw !important;
251
287
  height: 100vh !important;
252
288
  height: 100dvh !important;
289
+ box-sizing: border-box !important;
290
+ padding-top: 0 !important;
291
+ padding-top: var(--qzsy-safe-top, 0px) !important;
292
+ padding-bottom: 0 !important;
293
+ padding-bottom: var(--qzsy-safe-bottom, 0px) !important;
294
+ padding-left: 0 !important;
295
+ padding-left: var(--qzsy-safe-left, 0px) !important;
253
296
  z-index: 60 !important;
254
297
  background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
255
298
  border-right: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.08)) !important;
256
299
  box-shadow: none !important;
257
300
  transform: translateX(-102%) !important;
258
- transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1) !important;
259
301
  overflow: hidden !important;
260
302
  overscroll-behavior: contain;
261
303
  will-change: transform;
262
304
  }
305
+ /* \u9996\u5E27\u5148\u5C31\u4F4D\u3001\u4E0D\u64AD\u653E\u5165\u573A\u52A8\u753B\uFF0C\u907F\u514D\u9875\u9762\u52A0\u8F7D\u65F6\u4FA7\u680F\u4ECE 0 \u6ED1\u51FA */
306
+ body.qzsy-drawer-ready div[class*="_sidebarCol"] {
307
+ transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1) !important;
308
+ }
263
309
  body.qzsy-drawer-open div[class*="_sidebarCol"] {
264
310
  transform: translateX(0) !important;
265
311
  box-shadow: 8px 0 32px rgba(0, 0, 0, 0.18) !important;
@@ -268,6 +314,17 @@ var MOBILE_DRAWER_CSS = `
268
314
  width: 100% !important;
269
315
  }
270
316
 
317
+ /* \u62D6\u52A8\u4E2D\uFF1A\u5173\u95ED\u8FC7\u6E21\uFF0Ctransform/\u906E\u7F69\u76F4\u63A5\u8DDF\u624B */
318
+ body.qzsy-drawer-dragging div[class*="_sidebarCol"] {
319
+ transition: none !important;
320
+ transform: translateX(var(--qzsy-drawer-x, -102%)) !important;
321
+ }
322
+ body.qzsy-drawer-dragging .qzsy-mobile-backdrop {
323
+ transition: none !important;
324
+ opacity: var(--qzsy-drawer-progress, 0) !important;
325
+ pointer-events: auto !important;
326
+ }
327
+
271
328
  /* 3. \u534A\u900F\u660E\u906E\u7F69\uFF1A\u70B9\u51FB\u6536\u8D77 */
272
329
  .qzsy-mobile-backdrop {
273
330
  display: block;
@@ -286,17 +343,13 @@ var MOBILE_DRAWER_CSS = `
286
343
  pointer-events: auto;
287
344
  }
288
345
 
289
- /* 4. \u9876\u90E8\u83DC\u5355\u6309\u94AE\uFF08\u4F4D\u4E8E 52px \u7559\u767D\u6761\u5185\uFF0C\u907F\u514D\u906E\u6321\u4F1A\u8BDD\u6807\u9898\uFF09 */
346
+ /* 4. \u83DC\u5355\u6309\u94AE\uFF1A\u4F4D\u4E8E\u5B89\u5168\u533A\u4E0B\u65B9\u7684 52px \u7559\u767D\u6761\u5185 */
290
347
  .qzsy-mobile-menu-btn {
291
348
  display: inline-flex;
292
- position: fixed;
293
- top: 10px;
294
- left: 10px;
295
- width: 40px;
296
- height: 40px;
297
- z-index: 65;
298
349
  align-items: center;
299
350
  justify-content: center;
351
+ width: 40px;
352
+ height: 40px;
300
353
  padding: 0;
301
354
  border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
302
355
  border-radius: 12px;
@@ -304,6 +357,7 @@ var MOBILE_DRAWER_CSS = `
304
357
  color: var(--dsw-alias-label-primary, #111827);
305
358
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
306
359
  cursor: pointer;
360
+ pointer-events: auto;
307
361
  -webkit-tap-highlight-color: transparent;
308
362
  transition: opacity 180ms ease, transform 180ms ease;
309
363
  }
@@ -313,6 +367,109 @@ var MOBILE_DRAWER_CSS = `
313
367
  opacity: 0;
314
368
  pointer-events: none;
315
369
  }
370
+
371
+ /* 5. \u6A21\u578B\u5207\u6362\u5668\uFF1A\u628A\u5BBF\u4E3B\u684C\u9762 popover \u6539\u6210\u79FB\u52A8\u7AEF\u5E95\u90E8 sheet */
372
+ @keyframes qzsy-model-sheet-in {
373
+ from { transform: translateY(100%); opacity: 0.6; }
374
+ to { transform: translateY(0); opacity: 1; }
375
+ }
376
+
377
+ .qzsy-model-sheet-backdrop {
378
+ display: block;
379
+ position: fixed;
380
+ inset: 0;
381
+ z-index: 1190;
382
+ background: rgba(0, 0, 0, 0.4);
383
+ opacity: 0;
384
+ pointer-events: none;
385
+ transition: opacity 240ms ease;
386
+ touch-action: none;
387
+ -webkit-tap-highlight-color: transparent;
388
+ }
389
+ body.qzsy-model-sheet-open .qzsy-model-sheet-backdrop {
390
+ opacity: 1;
391
+ pointer-events: auto;
392
+ }
393
+
394
+ [data-qzsy-model-sheet] {
395
+ position: fixed !important;
396
+ top: auto !important;
397
+ left: 0 !important;
398
+ right: 0 !important;
399
+ bottom: 0 !important;
400
+ width: 100vw !important;
401
+ max-width: 100vw !important;
402
+ height: auto !important;
403
+ max-height: 82vh !important;
404
+ max-height: 82dvh !important;
405
+ margin: 0 !important;
406
+ padding: 18px 0 0 !important;
407
+ border: 0 !important;
408
+ border-radius: 18px 18px 0 0 !important;
409
+ background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
410
+ box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.18) !important;
411
+ box-sizing: border-box !important;
412
+ display: flex !important;
413
+ flex-direction: column !important;
414
+ overflow: hidden !important;
415
+ z-index: 1200 !important;
416
+ transform-origin: bottom center !important;
417
+ animation: qzsy-model-sheet-in 260ms cubic-bezier(0.16, 1, 0.3, 1) !important;
418
+ will-change: transform;
419
+ }
420
+ [data-qzsy-model-sheet]::before {
421
+ content: '';
422
+ position: absolute;
423
+ top: 7px;
424
+ left: 50%;
425
+ width: 36px;
426
+ height: 4px;
427
+ margin-left: -18px;
428
+ border-radius: 999px;
429
+ background: rgba(0, 0, 0, 0.18);
430
+ pointer-events: none;
431
+ }
432
+ [data-qzsy-model-sheet] > div,
433
+ [data-qzsy-model-sheet] div[role="menu"] {
434
+ width: 100% !important;
435
+ max-width: 100% !important;
436
+ border: 0 !important;
437
+ border-radius: 0 !important;
438
+ background: transparent !important;
439
+ box-shadow: none !important;
440
+ box-sizing: border-box !important;
441
+ }
442
+ [data-qzsy-model-sheet] div[role="menu"] {
443
+ max-height: calc(82vh - 40px) !important;
444
+ max-height: calc(82dvh - 40px) !important;
445
+ overflow-y: auto !important;
446
+ overscroll-behavior: contain !important;
447
+ padding: 8px 12px calc(14px + var(--qzsy-safe-bottom, 0px)) !important;
448
+ }
449
+ [data-qzsy-model-sheet] input {
450
+ width: 100% !important;
451
+ box-sizing: border-box !important;
452
+ }
453
+ [data-qzsy-model-sheet] button[role="menuitem"],
454
+ [data-qzsy-model-sheet] [role="option"] {
455
+ width: 100% !important;
456
+ box-sizing: border-box !important;
457
+ }
458
+ [data-qzsy-model-sheet].qzsy-model-sheet-closing {
459
+ transform: translateY(100%) !important;
460
+ opacity: 0 !important;
461
+ transition: transform 220ms ease, opacity 220ms ease !important;
462
+ animation: none !important;
463
+ }
464
+ body.qzsy-model-sheet-dragging [data-qzsy-model-sheet] {
465
+ transform: translateY(var(--qzsy-model-sheet-y, 0px)) !important;
466
+ transition: none !important;
467
+ animation: none !important;
468
+ }
469
+ body.qzsy-model-sheet-dragging .qzsy-model-sheet-backdrop {
470
+ opacity: var(--qzsy-model-sheet-progress, 1) !important;
471
+ transition: none !important;
472
+ }
316
473
  }
317
474
  `;
318
475
  function isElement(value) {
@@ -336,6 +493,10 @@ function injectStyles() {
336
493
  }
337
494
  if (style.textContent !== MOBILE_DRAWER_CSS) style.textContent = MOBILE_DRAWER_CSS;
338
495
  }
496
+ function drawerWidth() {
497
+ if (typeof window === "undefined") return 300;
498
+ return Math.min(300, Math.round(window.innerWidth * 0.84));
499
+ }
339
500
  function setupMobileDrawer() {
340
501
  if (typeof document === "undefined" || typeof window === "undefined") return () => {
341
502
  };
@@ -346,16 +507,29 @@ function setupMobileDrawer() {
346
507
  let menuBtn = null;
347
508
  let observer = null;
348
509
  let disposed = false;
510
+ let gesture = null;
511
+ let suppressClickUntil = 0;
512
+ let modelPortal = null;
513
+ let modelBackdrop = null;
514
+ let modelChainUntil = 0;
515
+ let modelDrag = null;
516
+ let modelDragHandlers = null;
349
517
  const ensureElements = () => {
350
518
  if (disposed || !document.body) return;
351
519
  injectStyles();
352
- let btn = document.querySelector(`.${MENU_CLASS}`);
520
+ let header = document.querySelector(`.${HEADER_CLASS}`);
521
+ if (!header) {
522
+ header = document.createElement("div");
523
+ header.className = HEADER_CLASS;
524
+ document.body.appendChild(header);
525
+ }
526
+ let btn = header.querySelector(`.${MENU_CLASS}`);
353
527
  if (!btn) {
354
528
  btn = document.createElement("button");
355
529
  btn.type = "button";
356
530
  btn.className = MENU_CLASS;
357
531
  btn.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><line x1="3" y1="8" x2="21" y2="8"></line><line x1="3" y1="15" x2="15" y2="15"></line></svg>';
358
- document.body.appendChild(btn);
532
+ header.appendChild(btn);
359
533
  }
360
534
  let mask = document.querySelector(`.${BACKDROP_CLASS}`);
361
535
  if (!mask) {
@@ -386,6 +560,15 @@ function setupMobileDrawer() {
386
560
  }
387
561
  }
388
562
  };
563
+ const setDragProgress = (x, width) => {
564
+ document.body.style.setProperty("--qzsy-drawer-x", `${x}px`);
565
+ document.body.style.setProperty("--qzsy-drawer-progress", String(Math.max(0, Math.min(1, 1 - Math.abs(x) / width))));
566
+ };
567
+ const clearDrag = () => {
568
+ document.body.classList.remove(DRAGGING_CLASS);
569
+ document.body.style.removeProperty("--qzsy-drawer-x");
570
+ document.body.style.removeProperty("--qzsy-drawer-progress");
571
+ };
389
572
  const openDrawer = () => {
390
573
  if (disposed || !isMobileViewport() || !document.body) return;
391
574
  ensureElements();
@@ -410,7 +593,225 @@ function setupMobileDrawer() {
410
593
  if (!disposed && isOpen()) closeDrawer({ restoreFocus: false });
411
594
  }, 60);
412
595
  };
596
+ const bodyPortalOf = (node) => {
597
+ let el = node;
598
+ while (el && el.parentElement && el.parentElement !== document.body) el = el.parentElement;
599
+ return el && el.parentElement === document.body ? el : null;
600
+ };
601
+ const visiblePortal = (el) => {
602
+ if (!el || !el.isConnected) return false;
603
+ const s = getComputedStyle(el);
604
+ if (s.display === "none" || s.visibility === "hidden") return false;
605
+ const r = el.getBoundingClientRect();
606
+ return r.width > 0 && r.height > 0;
607
+ };
608
+ const isModelTrigger = (target) => {
609
+ const el = target?.closest?.('button, [role="menuitem"], [role="button"], a');
610
+ if (!el) return false;
611
+ const label = `${el.getAttribute("aria-label") || ""} ${el.getAttribute("title") || ""} ${el.innerText || ""}`;
612
+ return /模型|Model|model/.test(label);
613
+ };
614
+ const findModelPortal = () => {
615
+ if (!isMobileViewport()) return null;
616
+ const search = document.querySelector(
617
+ 'input[placeholder*="\u641C\u7D22\u6A21\u578B"], input[placeholder*="Search model"], input[aria-label*="\u641C\u7D22\u6A21\u578B"], input[aria-label*="Search model"]'
618
+ );
619
+ if (search) {
620
+ const menu = search.closest('[role="menu"]') || search.closest('[class*="_menu"]');
621
+ const portal = bodyPortalOf(menu || search);
622
+ if (portal && visiblePortal(portal)) {
623
+ const s = getComputedStyle(portal);
624
+ if ((s.position === "fixed" || s.position === "absolute") && portal.getBoundingClientRect().width > 120) return portal;
625
+ }
626
+ }
627
+ if (Date.now() > modelChainUntil) return null;
628
+ const candidates = [];
629
+ for (const el of document.body.children) {
630
+ if (!(el instanceof HTMLElement)) continue;
631
+ if (el === modelBackdrop || el.classList.contains(HEADER_CLASS) || el.classList.contains(BACKDROP_CLASS)) continue;
632
+ if (!visiblePortal(el)) continue;
633
+ const s = getComputedStyle(el);
634
+ if (s.position !== "fixed" && s.position !== "absolute") continue;
635
+ const menu = el.querySelector('[role="menu"], [role="listbox"]');
636
+ if (!menu) continue;
637
+ const text = menu.innerText || "";
638
+ const rows = menu.querySelectorAll('[role="menuitem"], [role="option"], button, a, [tabindex]').length;
639
+ const hasSearch = Boolean(el.querySelector('input[placeholder*="\u641C\u7D22\u6A21\u578B"], input[placeholder*="Search model"]'));
640
+ if (/推理等级|Reasoning effort|推理强度/i.test(text) && rows <= 2) continue;
641
+ const rect = el.getBoundingClientRect();
642
+ const alreadySheet = rect.width >= window.innerWidth - 2 && Math.abs(rect.bottom - window.innerHeight) < 2 && rect.height > 120;
643
+ if (alreadySheet && !hasSearch) continue;
644
+ if (hasSearch || rows > 2) candidates.push({ el, rows, hasSearch });
645
+ }
646
+ candidates.sort((a, b) => Number(b.hasSearch) - Number(a.hasSearch) || b.rows - a.rows);
647
+ return candidates[0]?.el || null;
648
+ };
649
+ const hostBackdropPresent = () => {
650
+ for (const el of document.body.children) {
651
+ if (!(el instanceof HTMLElement)) continue;
652
+ if (el === modelBackdrop || el.classList.contains(HEADER_CLASS) || el.classList.contains(BACKDROP_CLASS)) continue;
653
+ const s = getComputedStyle(el);
654
+ if (s.position !== "fixed") continue;
655
+ const r = el.getBoundingClientRect();
656
+ if (r.width < window.innerWidth - 4 || r.height < window.innerHeight - 4) continue;
657
+ if (Number(s.opacity) < 0.1) continue;
658
+ const bg = s.backgroundColor || "";
659
+ if (!/^rgba?\(/.test(bg) || /rgba\(0,\s*0,\s*0,\s*0\)/.test(bg)) continue;
660
+ if (el.querySelector('[role="menu"]')) continue;
661
+ return true;
662
+ }
663
+ return false;
664
+ };
665
+ const requestHostModelClose = (portal) => {
666
+ if (disposed || !portal || !portal.isConnected) return;
667
+ const PointerCtor = typeof window.PointerEvent === "function" ? window.PointerEvent : window.MouseEvent;
668
+ try {
669
+ document.body.dispatchEvent(new PointerCtor("pointerdown", { bubbles: true, cancelable: true }));
670
+ } catch {
671
+ }
672
+ try {
673
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }));
674
+ } catch {
675
+ }
676
+ window.setTimeout(() => {
677
+ if (disposed || !portal.isConnected) return;
678
+ const trigger = document.querySelector('button[aria-haspopup="menu"][aria-expanded="true"][aria-label*="\u9009\u62E9\u6A21\u578B"], button[aria-haspopup="menu"][aria-expanded="true"][aria-label*="Select model"]');
679
+ try {
680
+ trigger?.click();
681
+ } catch {
682
+ }
683
+ window.setTimeout(() => {
684
+ if (!disposed && portal.isConnected && modelPortal === portal) clearModelSheet();
685
+ }, 450);
686
+ }, 120);
687
+ };
688
+ const closeModelSheetNow = () => {
689
+ const portal = modelPortal;
690
+ if (!portal) return;
691
+ document.body.classList.remove("qzsy-model-sheet-open");
692
+ document.body.style.removeProperty("--qzsy-model-sheet-y");
693
+ document.body.style.removeProperty("--qzsy-model-sheet-progress");
694
+ requestHostModelClose(portal);
695
+ };
696
+ const closeModelSheetAnimated = () => {
697
+ const portal = modelPortal;
698
+ if (!portal) return;
699
+ portal.classList.add("qzsy-model-sheet-closing");
700
+ document.body.classList.remove("qzsy-model-sheet-open");
701
+ window.setTimeout(() => requestHostModelClose(portal), 180);
702
+ };
703
+ const ensureModelBackdrop = () => {
704
+ if (modelBackdrop || hostBackdropPresent()) return;
705
+ modelBackdrop = document.createElement("div");
706
+ modelBackdrop.className = "qzsy-model-sheet-backdrop";
707
+ modelBackdrop.setAttribute("aria-hidden", "true");
708
+ modelBackdrop.addEventListener("pointerdown", (event) => event.stopPropagation(), true);
709
+ modelBackdrop.addEventListener("click", (event) => {
710
+ event.preventDefault();
711
+ event.stopPropagation();
712
+ closeModelSheetNow();
713
+ });
714
+ document.body.appendChild(modelBackdrop);
715
+ };
716
+ const detachModelDrag = () => {
717
+ if (!modelPortal || !modelDragHandlers) return;
718
+ modelPortal.removeEventListener("touchstart", modelDragHandlers.onStart);
719
+ modelPortal.removeEventListener("touchmove", modelDragHandlers.onMove);
720
+ modelPortal.removeEventListener("touchend", modelDragHandlers.onEnd);
721
+ modelPortal.removeEventListener("touchcancel", modelDragHandlers.onEnd);
722
+ modelDragHandlers = null;
723
+ };
724
+ const attachModelDrag = (portal) => {
725
+ if (!portal || modelDragHandlers) return;
726
+ const onStart = (event) => {
727
+ if (event.touches?.length !== 1) return;
728
+ const touch = event.touches[0];
729
+ const rect = portal.getBoundingClientRect();
730
+ if (touch.clientY - rect.top > 30) return;
731
+ if (event.target.closest?.('input, textarea, select, button, a, [contenteditable="true"]')) return;
732
+ modelDrag = { startY: touch.clientY, startT: Date.now(), dy: 0, active: false, height: rect.height };
733
+ };
734
+ const onMove = (event) => {
735
+ if (!modelDrag) return;
736
+ const touch = event.touches?.[0];
737
+ if (!touch) return;
738
+ const dy = touch.clientY - modelDrag.startY;
739
+ if (!modelDrag.active) {
740
+ if (dy < 6) return;
741
+ modelDrag.active = true;
742
+ document.body.classList.add("qzsy-model-sheet-dragging");
743
+ }
744
+ modelDrag.dy = Math.max(0, dy);
745
+ document.body.style.setProperty("--qzsy-model-sheet-y", `${modelDrag.dy}px`);
746
+ document.body.style.setProperty("--qzsy-model-sheet-progress", String(Math.max(0, 1 - modelDrag.dy / Math.max(1, modelDrag.height))));
747
+ if (event.cancelable) event.preventDefault();
748
+ };
749
+ const onEnd = () => {
750
+ if (!modelDrag) return;
751
+ const drag = modelDrag;
752
+ modelDrag = null;
753
+ if (!drag.active) return;
754
+ document.body.classList.remove("qzsy-model-sheet-dragging");
755
+ document.body.style.removeProperty("--qzsy-model-sheet-y");
756
+ document.body.style.removeProperty("--qzsy-model-sheet-progress");
757
+ const velocity = drag.dy / Math.max(1, Date.now() - drag.startT);
758
+ if (drag.dy > 110 || velocity > 0.5) closeModelSheetAnimated();
759
+ };
760
+ portal.addEventListener("touchstart", onStart, { passive: true });
761
+ portal.addEventListener("touchmove", onMove, { passive: false });
762
+ portal.addEventListener("touchend", onEnd, { passive: true });
763
+ portal.addEventListener("touchcancel", onEnd, { passive: true });
764
+ modelDragHandlers = { onStart, onMove, onEnd };
765
+ };
766
+ const clearModelSheet = () => {
767
+ if (modelPortal) {
768
+ detachModelDrag();
769
+ modelPortal.removeAttribute("data-qzsy-model-sheet");
770
+ modelPortal.classList.remove("qzsy-model-sheet", "qzsy-model-sheet-closing");
771
+ }
772
+ modelPortal = null;
773
+ modelDrag = null;
774
+ document.body?.classList.remove("qzsy-model-sheet-open", "qzsy-model-sheet-dragging");
775
+ document.body?.style.removeProperty("--qzsy-model-sheet-y");
776
+ document.body?.style.removeProperty("--qzsy-model-sheet-progress");
777
+ if (modelBackdrop) {
778
+ modelBackdrop.remove();
779
+ modelBackdrop = null;
780
+ }
781
+ };
782
+ const applyModelSheet = (portal) => {
783
+ if (modelPortal === portal) return;
784
+ clearModelSheet();
785
+ modelPortal = portal;
786
+ portal.setAttribute("data-qzsy-model-sheet", "");
787
+ portal.classList.add("qzsy-model-sheet");
788
+ attachModelDrag(portal);
789
+ ensureModelBackdrop();
790
+ window.requestAnimationFrame(() => {
791
+ if (!disposed && modelPortal === portal) document.body.classList.add("qzsy-model-sheet-open");
792
+ });
793
+ };
794
+ const syncModelSheet = () => {
795
+ if (disposed) return;
796
+ if (!isMobileViewport()) {
797
+ clearModelSheet();
798
+ return;
799
+ }
800
+ if (modelPortal && !visiblePortal(modelPortal)) clearModelSheet();
801
+ const found = findModelPortal();
802
+ if (!found) {
803
+ if (modelPortal?.isConnected) return;
804
+ if (modelPortal) clearModelSheet();
805
+ return;
806
+ }
807
+ if (found !== modelPortal) applyModelSheet(found);
808
+ };
413
809
  const onClick = (event) => {
810
+ if (Date.now() < suppressClickUntil) {
811
+ event.preventDefault();
812
+ event.stopPropagation();
813
+ return;
814
+ }
414
815
  const target = event.target;
415
816
  if (!isElement(target)) return;
416
817
  if (target.closest(`.${MENU_CLASS}`)) {
@@ -419,6 +820,11 @@ function setupMobileDrawer() {
419
820
  else openDrawer();
420
821
  return;
421
822
  }
823
+ if (isModelTrigger(target)) {
824
+ modelChainUntil = Date.now() + 6e3;
825
+ window.setTimeout(syncModelSheet, 30);
826
+ window.setTimeout(syncModelSheet, 180);
827
+ }
422
828
  if (!isOpen()) return;
423
829
  if (target.closest(`.${BACKDROP_CLASS}`)) {
424
830
  closeDrawer();
@@ -447,10 +853,108 @@ function setupMobileDrawer() {
447
853
  if (event.key === "Escape" && isOpen()) closeDrawer();
448
854
  };
449
855
  const onResize = () => {
450
- if (!isMobileViewport() && isOpen()) closeDrawer({ restoreFocus: false });
856
+ if (!isMobileViewport() && isOpen()) {
857
+ gesture = null;
858
+ clearDrag();
859
+ closeDrawer({ restoreFocus: false });
860
+ }
861
+ syncModelSheet();
451
862
  };
452
863
  const onVisibilityChange = () => {
453
- if (document.visibilityState === "hidden" && isOpen()) closeDrawer({ restoreFocus: false });
864
+ if (document.visibilityState === "hidden" && isOpen()) {
865
+ gesture = null;
866
+ clearDrag();
867
+ closeDrawer({ restoreFocus: false });
868
+ }
869
+ if (document.visibilityState === "visible") syncModelSheet();
870
+ };
871
+ const onTouchStart = (event) => {
872
+ if (modelPortal || disposed || !isMobileViewport() || event.touches?.length !== 1) return;
873
+ const touch = event.touches[0];
874
+ const sidebar = document.querySelector('div[class*="_sidebarCol"]');
875
+ if (!sidebar) return;
876
+ const open = isOpen();
877
+ if (!open) {
878
+ if (touch.clientX > EDGE_START_ZONE) return;
879
+ ensureSidebarExpanded();
880
+ gesture = {
881
+ mode: "open",
882
+ startX: touch.clientX,
883
+ startY: touch.clientY,
884
+ startT: Date.now(),
885
+ width: drawerWidth(),
886
+ active: false,
887
+ dx: 0
888
+ };
889
+ return;
890
+ }
891
+ if (!sidebar.contains(event.target)) return;
892
+ if (event.target.closest?.('input, textarea, select, [contenteditable="true"]')) return;
893
+ gesture = {
894
+ mode: "close",
895
+ startX: touch.clientX,
896
+ startY: touch.clientY,
897
+ startT: Date.now(),
898
+ width: drawerWidth(),
899
+ active: false,
900
+ dx: 0
901
+ };
902
+ };
903
+ const onTouchMove = (event) => {
904
+ if (modelPortal || !gesture || disposed) return;
905
+ const touch = event.touches?.[0];
906
+ if (!touch) return;
907
+ const dx = touch.clientX - gesture.startX;
908
+ const dy = touch.clientY - gesture.startY;
909
+ if (!gesture.active) {
910
+ if (Math.abs(dx) < DRAG_THRESHOLD) return;
911
+ if (Math.abs(dx) < Math.abs(dy) * 1.2) {
912
+ gesture = null;
913
+ return;
914
+ }
915
+ if (gesture.mode === "open" && dx <= 0) {
916
+ gesture = null;
917
+ return;
918
+ }
919
+ if (gesture.mode === "close" && dx >= 0) {
920
+ gesture = null;
921
+ return;
922
+ }
923
+ gesture.active = true;
924
+ suppressClickUntil = Date.now() + 450;
925
+ document.body.classList.add(DRAGGING_CLASS);
926
+ }
927
+ const width = Math.max(1, gesture.width);
928
+ let x;
929
+ if (gesture.mode === "open") x = Math.max(-width, Math.min(0, -width + dx));
930
+ else x = Math.max(-width, Math.min(0, dx));
931
+ gesture.dx = dx;
932
+ setDragProgress(x, width);
933
+ };
934
+ const finishGesture = (touch) => {
935
+ if (!gesture) return;
936
+ const current = gesture;
937
+ gesture = null;
938
+ if (!current.active) return;
939
+ const endX = touch ? touch.clientX : current.startX + current.dx;
940
+ const dx = endX - current.startX;
941
+ const elapsed = Math.max(1, Date.now() - current.startT);
942
+ const velocity = dx / elapsed;
943
+ const threshold = Math.min(90, Math.max(60, current.width * 0.3));
944
+ const openNow = current.mode === "open" ? dx > threshold || velocity > 0.45 : !(dx < -Math.max(50, threshold * 0.7) || velocity < -0.35);
945
+ document.body.classList.toggle(OPEN_CLASS, openNow);
946
+ if (openNow) ensureSidebarExpanded();
947
+ syncAria();
948
+ suppressClickUntil = Date.now() + 250;
949
+ window.requestAnimationFrame(clearDrag);
950
+ };
951
+ const onTouchEnd = (event) => {
952
+ if (modelPortal || !gesture) return;
953
+ finishGesture(event.changedTouches?.[0]);
954
+ };
955
+ const onTouchCancel = () => {
956
+ if (modelPortal || !gesture) return;
957
+ finishGesture(null);
454
958
  };
455
959
  injectStyles();
456
960
  ensureElements();
@@ -458,17 +962,27 @@ function setupMobileDrawer() {
458
962
  document.addEventListener("keydown", onKeyDown, true);
459
963
  document.addEventListener("visibilitychange", onVisibilityChange);
460
964
  window.addEventListener("resize", onResize);
965
+ window.addEventListener("touchstart", onTouchStart, { passive: true });
966
+ window.addEventListener("touchmove", onTouchMove, { passive: true });
967
+ window.addEventListener("touchend", onTouchEnd, { passive: true });
968
+ window.addEventListener("touchcancel", onTouchCancel, { passive: true });
969
+ window.requestAnimationFrame(() => {
970
+ if (!disposed && document.body) document.body.classList.add("qzsy-drawer-ready");
971
+ });
461
972
  if (typeof MutationObserver === "function" && document.body) {
462
973
  observer = new MutationObserver(() => {
463
- if (!document.querySelector(`.${MENU_CLASS}`) || !document.querySelector(`.${BACKDROP_CLASS}`) || !document.getElementById(MOBILE_DRAWER_CSS_ID)) {
974
+ if (!document.querySelector(`.${HEADER_CLASS}`) || !document.querySelector(`.${MENU_CLASS}`) || !document.querySelector(`.${BACKDROP_CLASS}`) || !document.getElementById(MOBILE_DRAWER_CSS_ID)) {
464
975
  ensureElements();
465
976
  }
977
+ syncModelSheet();
466
978
  });
467
979
  observer.observe(document.body, { childList: true });
468
980
  }
981
+ syncModelSheet();
469
982
  syncAria();
470
983
  return () => {
471
984
  disposed = true;
985
+ gesture = null;
472
986
  try {
473
987
  observer?.disconnect();
474
988
  } catch {
@@ -477,8 +991,15 @@ function setupMobileDrawer() {
477
991
  document.removeEventListener("keydown", onKeyDown, true);
478
992
  document.removeEventListener("visibilitychange", onVisibilityChange);
479
993
  window.removeEventListener("resize", onResize);
994
+ window.removeEventListener("touchstart", onTouchStart);
995
+ window.removeEventListener("touchmove", onTouchMove);
996
+ window.removeEventListener("touchend", onTouchEnd);
997
+ window.removeEventListener("touchcancel", onTouchCancel);
998
+ clearModelSheet();
999
+ clearDrag();
480
1000
  document.body?.classList.remove(OPEN_CLASS);
481
- document.querySelector(`.${MENU_CLASS}`)?.remove();
1001
+ document.body?.classList.remove("qzsy-drawer-ready");
1002
+ document.querySelector(`.${HEADER_CLASS}`)?.remove();
482
1003
  document.querySelector(`.${BACKDROP_CLASS}`)?.remove();
483
1004
  document.getElementById(MOBILE_DRAWER_CSS_ID)?.remove();
484
1005
  root.removeAttribute(MOBILE_UI_FLAG);