hyper-scheduler 1.0.1 → 1.1.1

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="docs/public/logo.svg" width="120" height="120" alt="Hyper Scheduler Logo">
2
+ <img src="public/logo.svg" width="120" height="120" alt="Hyper Scheduler Logo">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Hyper Scheduler</h1>
@@ -14,6 +14,8 @@ A lightweight, dependency-free (core) JavaScript task scheduler supporting Cron
14
14
  ## Features
15
15
  - 🚀 **Cross-platform**: Works in Node.js and Browser.
16
16
  - ⏰ **Precise Timing**: Uses Web Workers in browser to avoid background throttling.
17
+ - 🏷️ **Namespaces**: Isolate tasks into logical groups for batch control.
18
+ - ⚡ **Immediate Trigger**: Option to execute tasks immediately upon start.
17
19
  - 🛠 **Debuggable**: Built-in debug panel and CLI output.
18
20
  - 📦 **Tiny**: < 20KB gzipped.
19
21
 
@@ -27,12 +29,27 @@ npm install hyper-scheduler
27
29
  import { Scheduler } from 'hyper-scheduler';
28
30
 
29
31
  const scheduler = new Scheduler({ debug: true });
32
+
33
+ // Standard task
30
34
  scheduler.createTask({
31
35
  id: 'hello',
32
36
  schedule: '*/5 * * * * *',
33
37
  handler: () => console.log('Hello World')
34
38
  });
39
+
40
+ // Task in a namespace with immediate execution
41
+ scheduler.createTask({
42
+ id: 'system-check',
43
+ schedule: '1h',
44
+ handler: () => console.log('System Check'),
45
+ options: {
46
+ namespace: 'system',
47
+ runImmediately: true
48
+ }
49
+ });
50
+
35
51
  scheduler.start();
52
+ // Or start only 'system' namespace: scheduler.start('system');
36
53
  ```
37
54
 
38
55
  See [Documentation](docs/guide/getting-started.md) for more details.
@@ -327,6 +327,9 @@ const themeStyles = `
327
327
  --hs-panel-width: 400px;
328
328
  --hs-panel-height: 300px;
329
329
 
330
+ /* 等宽数字字体 */
331
+ --hs-font-monospaced-num: var(--hs-font-mono);
332
+
330
333
  /* Light Theme (Default) */
331
334
  --hs-bg: #ffffff;
332
335
  --hs-bg-secondary: #f3f4f6;
@@ -344,6 +347,7 @@ const themeStyles = `
344
347
  --hs-radius: 6px;
345
348
  --hs-header-height: 40px;
346
349
  --hs-z-index: 9999;
350
+ --hs-z-index-overlay: 9998;
347
351
 
348
352
  /* Default display styles for the host itself */
349
353
  background: var(--hs-bg);
@@ -387,6 +391,8 @@ const ICONS = {
387
391
  dockRight: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="18" rx="2" ry="2"></rect><line x1="15" y1="3" x2="15" y2="21"></line></svg>`,
388
392
  chart: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>`
389
393
  };
394
+ const STORAGE_KEY_POS = "hs-trigger-position";
395
+ const STORAGE_KEY_COLLAPSED = "hs-trigger-collapsed";
390
396
  class FloatingTrigger extends HTMLElement {
391
397
  constructor() {
392
398
  super();
@@ -395,43 +401,230 @@ class FloatingTrigger extends HTMLElement {
395
401
  __publicField(this, "_position", "bottom-right");
396
402
  __publicField(this, "_bgColor", "");
397
403
  __publicField(this, "_textColor", "");
404
+ __publicField(this, "_isDragging", false);
405
+ __publicField(this, "_wasDragging", false);
406
+ // 标记是否发生过拖拽
407
+ __publicField(this, "_offsetX", 0);
408
+ __publicField(this, "_offsetY", 0);
409
+ __publicField(this, "_isCollapsed", false);
398
410
  this._shadow = this.attachShadow({ mode: "open" });
399
411
  }
412
+ // 新增:收起状态
400
413
  static get observedAttributes() {
401
414
  return ["position", "bg-color", "text-color"];
402
415
  }
403
416
  connectedCallback() {
417
+ this.loadState();
404
418
  this.render();
405
419
  this.addEventListeners();
420
+ this.applyPosition();
421
+ window.addEventListener("resize", this.onResize.bind(this));
422
+ }
423
+ disconnectedCallback() {
424
+ window.removeEventListener("resize", this.onResize.bind(this));
425
+ }
426
+ onResize() {
427
+ this.applyPosition();
406
428
  }
407
429
  attributeChangedCallback(name, _oldVal, newVal) {
408
- console.log("[FloatingTrigger] attributeChangedCallback:", name, newVal);
409
430
  if (name === "position") {
410
431
  this._position = newVal || "bottom-right";
432
+ this.applyPosition();
411
433
  } else if (name === "bg-color") {
412
434
  this._bgColor = newVal || "";
435
+ this.updateStyles();
413
436
  } else if (name === "text-color") {
414
437
  this._textColor = newVal || "";
438
+ this.updateStyles();
415
439
  }
416
- if (this._shadow.querySelector("button")) {
417
- console.log("[FloatingTrigger] re-rendering with:", this._position, this._bgColor, this._textColor);
418
- this.render();
419
- this.addEventListeners();
440
+ }
441
+ loadState() {
442
+ try {
443
+ const savedPos = localStorage.getItem(STORAGE_KEY_POS);
444
+ if (savedPos) {
445
+ const { x, y } = JSON.parse(savedPos);
446
+ this.style.setProperty("--hs-trigger-left", `${x}px`);
447
+ this.style.setProperty("--hs-trigger-top", `${y}px`);
448
+ this.style.setProperty("--hs-trigger-position-set", "true");
449
+ }
450
+ const savedCollapsed = localStorage.getItem(STORAGE_KEY_COLLAPSED);
451
+ if (savedCollapsed === "true") {
452
+ this._isCollapsed = true;
453
+ }
454
+ } catch (e) {
455
+ console.warn("[FloatingTrigger] Failed to load state:", e);
456
+ }
457
+ }
458
+ saveState() {
459
+ const button = this._shadow.querySelector("button");
460
+ if (!button) return;
461
+ localStorage.setItem(STORAGE_KEY_COLLAPSED, String(this._isCollapsed));
462
+ if (!this._isCollapsed) {
463
+ const rect = button.getBoundingClientRect();
464
+ localStorage.setItem(STORAGE_KEY_POS, JSON.stringify({ x: rect.left, y: rect.top }));
465
+ } else {
466
+ try {
467
+ const savedPos = localStorage.getItem(STORAGE_KEY_POS);
468
+ if (savedPos) {
469
+ const { x } = JSON.parse(savedPos);
470
+ const rect = button.getBoundingClientRect();
471
+ localStorage.setItem(STORAGE_KEY_POS, JSON.stringify({ x, y: rect.top }));
472
+ }
473
+ } catch (e) {
474
+ }
475
+ }
476
+ }
477
+ applyPosition() {
478
+ const button = this._shadow.querySelector("button");
479
+ if (!button) return;
480
+ const maxX = window.innerWidth - button.offsetWidth;
481
+ const maxY = window.innerHeight - button.offsetHeight;
482
+ if (this._isCollapsed) {
483
+ let currentY = parseFloat(this.style.getPropertyValue("--hs-trigger-top") || "20");
484
+ currentY = Math.max(0, Math.min(currentY, maxY));
485
+ button.style.right = "0px";
486
+ button.style.left = "auto";
487
+ button.style.top = `${currentY}px`;
488
+ button.style.bottom = "auto";
489
+ this.style.setProperty("--hs-trigger-top", `${currentY}px`);
490
+ } else {
491
+ if (!this.style.getPropertyValue("--hs-trigger-position-set")) {
492
+ const pos = this._position;
493
+ button.style.top = pos.includes("top") ? "20px" : "auto";
494
+ button.style.bottom = pos.includes("bottom") ? "20px" : "auto";
495
+ button.style.left = pos.includes("left") ? "20px" : "auto";
496
+ button.style.right = pos.includes("right") ? "20px" : "auto";
497
+ } else {
498
+ let currentX = parseFloat(this.style.getPropertyValue("--hs-trigger-left") || "0");
499
+ let currentY = parseFloat(this.style.getPropertyValue("--hs-trigger-top") || "0");
500
+ currentX = Math.max(0, Math.min(currentX, maxX));
501
+ currentY = Math.max(0, Math.min(currentY, maxY));
502
+ this.style.setProperty("--hs-trigger-left", `${currentX}px`);
503
+ this.style.setProperty("--hs-trigger-top", `${currentY}px`);
504
+ button.style.left = `${currentX}px`;
505
+ button.style.top = `${currentY}px`;
506
+ button.style.right = "auto";
507
+ button.style.bottom = "auto";
508
+ }
509
+ }
510
+ this.updateCollapsedState();
511
+ }
512
+ updateStyles() {
513
+ const button = this._shadow.querySelector("button");
514
+ if (button) {
515
+ button.style.background = this._bgColor || "var(--hs-primary)";
516
+ button.style.color = this._textColor || "white";
517
+ if (this._bgColor) {
518
+ button.style.setProperty("--hs-trigger-bg-hover", `${this._bgColor}; filter: brightness(1.1);`);
519
+ } else {
520
+ button.style.removeProperty("--hs-trigger-bg-hover");
521
+ }
522
+ }
523
+ }
524
+ updateCollapsedState() {
525
+ const button = this._shadow.querySelector("button");
526
+ if (button) {
527
+ if (this._isCollapsed) {
528
+ button.classList.add("collapsed");
529
+ } else {
530
+ button.classList.remove("collapsed");
531
+ }
420
532
  }
421
533
  }
422
534
  addEventListeners() {
423
535
  const btn = this._shadow.querySelector("button");
424
- btn == null ? void 0 : btn.addEventListener("click", () => {
536
+ if (!btn) return;
537
+ btn.addEventListener("click", (e) => {
538
+ if (this._wasDragging) {
539
+ e.preventDefault();
540
+ e.stopPropagation();
541
+ this._wasDragging = false;
542
+ return;
543
+ }
425
544
  this.dispatchEvent(new CustomEvent("toggle", { bubbles: true, composed: true }));
426
545
  });
546
+ const collapseBtn = this._shadow.querySelector(".collapse-btn");
547
+ collapseBtn == null ? void 0 : collapseBtn.addEventListener("click", (e) => {
548
+ e.stopPropagation();
549
+ e.preventDefault();
550
+ this._isCollapsed = !this._isCollapsed;
551
+ this.applyPosition();
552
+ this.saveState();
553
+ });
554
+ btn.addEventListener("dblclick", (e) => {
555
+ if (this._isCollapsed) {
556
+ e.stopPropagation();
557
+ this._isCollapsed = false;
558
+ this.applyPosition();
559
+ this.saveState();
560
+ }
561
+ });
562
+ btn.addEventListener("mousedown", (e) => {
563
+ if (e.button !== 0) return;
564
+ if (e.target.closest(".collapse-btn")) return;
565
+ this._isDragging = true;
566
+ this._wasDragging = false;
567
+ this._offsetX = e.clientX - btn.getBoundingClientRect().left;
568
+ this._offsetY = e.clientY - btn.getBoundingClientRect().top;
569
+ const startX = e.clientX;
570
+ const startY = e.clientY;
571
+ let hasMoved = false;
572
+ const onMouseMove = (moveEvent) => {
573
+ if (!this._isDragging) return;
574
+ if (!hasMoved && (Math.abs(moveEvent.clientX - startX) > 2 || Math.abs(moveEvent.clientY - startY) > 2)) {
575
+ hasMoved = true;
576
+ this._wasDragging = true;
577
+ btn.style.cursor = "grabbing";
578
+ btn.style.transition = "none";
579
+ }
580
+ if (hasMoved) {
581
+ let newX = moveEvent.clientX - this._offsetX;
582
+ let newY = moveEvent.clientY - this._offsetY;
583
+ const maxX = window.innerWidth - btn.offsetWidth;
584
+ const maxY = window.innerHeight - btn.offsetHeight;
585
+ newX = Math.max(0, Math.min(newX, maxX));
586
+ newY = Math.max(0, Math.min(newY, maxY));
587
+ if (this._isCollapsed) {
588
+ btn.style.left = "auto";
589
+ btn.style.right = "0px";
590
+ } else {
591
+ btn.style.left = `${newX}px`;
592
+ btn.style.right = "auto";
593
+ this.style.setProperty("--hs-trigger-left", `${newX}px`);
594
+ }
595
+ btn.style.top = `${newY}px`;
596
+ btn.style.bottom = "auto";
597
+ this.style.setProperty("--hs-trigger-position-set", "true");
598
+ this.style.setProperty("--hs-trigger-top", `${newY}px`);
599
+ }
600
+ };
601
+ const onMouseUp = () => {
602
+ if (!this._isDragging) return;
603
+ this._isDragging = false;
604
+ btn.style.cursor = "pointer";
605
+ btn.style.transition = "";
606
+ if (hasMoved) {
607
+ this.saveState();
608
+ const preventClick = (e2) => {
609
+ e2.stopPropagation();
610
+ e2.preventDefault();
611
+ };
612
+ window.addEventListener("click", preventClick, { capture: true, once: true });
613
+ }
614
+ window.removeEventListener("mousemove", onMouseMove);
615
+ window.removeEventListener("mouseup", onMouseUp);
616
+ };
617
+ window.addEventListener("mousemove", onMouseMove);
618
+ window.addEventListener("mouseup", onMouseUp);
619
+ });
427
620
  }
428
621
  render() {
429
- const pos = this._position;
430
622
  const posStyles = `
431
- top: ${pos.includes("top") ? "20px" : "auto"};
432
- bottom: ${pos.includes("bottom") ? "20px" : "auto"};
433
- left: ${pos.includes("left") ? "20px" : "auto"};
434
- right: ${pos.includes("right") ? "20px" : "auto"};
623
+ top: var(--hs-trigger-top, ${this._position.includes("top") ? "20px" : "auto"});
624
+ bottom: var(--hs-trigger-bottom, ${this._position.includes("bottom") ? "20px" : "auto"});
625
+ left: var(--hs-trigger-left, ${this._position.includes("left") ? "20px" : "auto"});
626
+ right: var(--hs-trigger-right, ${this._position.includes("right") ? "20px" : "auto"});
627
+ transform: translateX(0);
435
628
  `;
436
629
  const bgStyle = this._bgColor ? `background: ${this._bgColor};` : "";
437
630
  const colorStyle = this._textColor ? `color: ${this._textColor};` : "";
@@ -443,33 +636,104 @@ class FloatingTrigger extends HTMLElement {
443
636
  ${posStyles}
444
637
  width: 48px;
445
638
  height: 48px;
446
- border-radius: 50%;
639
+ border-radius: 12px; /* 更现代的圆角 */
447
640
  background: var(--hs-primary);
448
641
  color: white;
449
642
  ${bgStyle}
450
643
  ${colorStyle}
451
644
  border: none;
452
- box-shadow: var(--hs-shadow);
645
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15); /* 更柔和的阴影 */
453
646
  cursor: pointer;
454
647
  display: flex;
455
648
  align-items: center;
456
649
  justify-content: center;
457
650
  font-family: var(--hs-font-family);
458
651
  z-index: var(--hs-z-index);
652
+ transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), background 0.2s, box-shadow 0.2s;
653
+ overflow: visible; /* 允许子元素(收起按钮)溢出或显示 */
654
+ }
655
+ .icon {
459
656
  transition: transform 0.2s;
657
+ display: flex;
460
658
  }
461
659
  button:hover {
462
660
  ${this._bgColor ? `background: ${this._bgColor}; filter: brightness(1.1);` : "background: var(--hs-primary-hover);"}
661
+ box-shadow: 0 6px 16px rgba(0,0,0,0.2);
662
+ }
663
+ button:hover .icon {
463
664
  transform: scale(1.1);
464
665
  }
465
- button:active {
666
+ button:active .icon {
466
667
  transform: scale(0.95);
467
668
  }
669
+
670
+ /* 显式收起按钮 */
671
+ .collapse-btn {
672
+ position: absolute;
673
+ top: 0;
674
+ right: 0;
675
+ width: 20px;
676
+ height: 20px;
677
+ background: rgba(0,0,0,0.1);
678
+ border-top-right-radius: 12px;
679
+ display: flex;
680
+ align-items: center;
681
+ justify-content: center;
682
+ opacity: 1; /* 默认可见 */
683
+ transition: opacity 0.2s, background 0.2s;
684
+ color: white;
685
+ font-weight: bold;
686
+ }
687
+ .collapse-btn::before {
688
+ content: '—'; /* 最小化图标 */
689
+ font-size: 12px;
690
+ color: currentColor;
691
+ }
692
+ button:hover .collapse-btn {
693
+ opacity: 1;
694
+ }
695
+ .collapse-btn:hover {
696
+ background: rgba(0,0,0,0.3);
697
+ }
698
+
699
+ /* 收起状态样式 */
700
+ button.collapsed {
701
+ width: 24px;
702
+ height: 48px;
703
+ border-radius: 12px 0 0 12px;
704
+ transform: none; /* 完全展示,不再隐藏 */
705
+ opacity: 1;
706
+ right: 0 !important; /* 强制右对齐 */
707
+ left: auto !important;
708
+ }
709
+ button.collapsed:hover {
710
+ width: 28px; /* hover 时稍微变宽 */
711
+ opacity: 1;
712
+ }
713
+ button.collapsed .icon {
714
+ display: none;
715
+ }
716
+ button.collapsed .collapse-btn {
717
+ position: static; /* 充满父容器 */
718
+ width: 100%;
719
+ height: 100%;
720
+ border-radius: 0;
721
+ background: transparent;
722
+ }
723
+ button.collapsed .collapse-btn::before {
724
+ content: '‹'; /* 展开图标 (左箭头) */
725
+ font-size: 20px;
726
+ line-height: 48px;
727
+ }
468
728
  </style>
469
729
  <button title="Toggle Hyper Scheduler DevTools">
470
- ${ICONS.chart}
730
+ <div class="collapse-btn" title="Minimize"></div>
731
+ <span class="icon">${ICONS.chart}</span>
471
732
  </button>
472
733
  `;
734
+ this.applyPosition();
735
+ this.updateCollapsedState();
736
+ this.updateStyles();
473
737
  }
474
738
  }
475
739
  customElements.define("hs-floating-trigger", FloatingTrigger);
@@ -504,7 +768,7 @@ class TaskHeader extends HTMLElement {
504
768
  this._fps = Math.round(val);
505
769
  if (this.$fps) {
506
770
  const color = this._fps < 30 ? "var(--hs-danger)" : this._fps < 50 ? "var(--hs-warning)" : "var(--hs-success)";
507
- this.$fps.innerHTML = `⚡ ${t("stats.fps")}: <span style="color:${color}">${this._fps}</span> (${t("stats.mainThread")})`;
771
+ this.$fps.innerHTML = `⚡ ${t("stats.fps")}: <span style="color:${color}; font-family:var(--hs-font-monospaced-num);">${this._fps}</span> (${t("stats.mainThread")})`;
508
772
  }
509
773
  }
510
774
  set stats(val) {
@@ -766,6 +1030,7 @@ class TaskList extends HTMLElement {
766
1030
  __publicField(this, "_shadow");
767
1031
  __publicField(this, "_tasks", []);
768
1032
  __publicField(this, "_lastExecutionTimes", /* @__PURE__ */ new Map());
1033
+ __publicField(this, "_expandedNamespaces", /* @__PURE__ */ new Set(["default"]));
769
1034
  this._shadow = this.attachShadow({ mode: "open" });
770
1035
  }
771
1036
  connectedCallback() {
@@ -782,6 +1047,17 @@ class TaskList extends HTMLElement {
782
1047
  this._tasks = newTasks;
783
1048
  this.renderRows();
784
1049
  }
1050
+ groupTasksByNamespace(tasks) {
1051
+ const groups = /* @__PURE__ */ new Map();
1052
+ tasks.forEach((task) => {
1053
+ const ns = task.namespace || "default";
1054
+ if (!groups.has(ns)) {
1055
+ groups.set(ns, []);
1056
+ }
1057
+ groups.get(ns).push(task);
1058
+ });
1059
+ return groups;
1060
+ }
785
1061
  filter(text, map) {
786
1062
  const all = Array.from(map.values());
787
1063
  if (!text) {
@@ -789,7 +1065,7 @@ class TaskList extends HTMLElement {
789
1065
  } else {
790
1066
  const lower = text.toLowerCase();
791
1067
  this._tasks = all.filter(
792
- (t2) => t2.id.toLowerCase().includes(lower) || t2.tags.some((tag) => tag.toLowerCase().includes(lower))
1068
+ (t2) => t2.id.toLowerCase().includes(lower) || t2.tags.some((tag) => tag.toLowerCase().includes(lower)) || t2.namespace && t2.namespace.toLowerCase().includes(lower)
793
1069
  );
794
1070
  }
795
1071
  this.renderRows();
@@ -862,18 +1138,21 @@ class TaskList extends HTMLElement {
862
1138
  }
863
1139
  return `<span class="driver-badge main" title="${t("list.driverMain")}">M</span>`;
864
1140
  }
865
- renderRows() {
866
- const tbody = this._shadow.querySelector("tbody");
867
- if (!tbody) return;
868
- tbody.innerHTML = this._tasks.map((task, index) => {
869
- const lastExec = this._lastExecutionTimes.get(task.id);
870
- const isRecentlyExecuted = lastExec && Date.now() - lastExec < 1e3;
871
- const rowClass = isRecentlyExecuted ? "recently-executed" : "";
872
- return `
873
- <tr data-id="${task.id}" class="${rowClass}">
874
- <td class="col-num">${index + 1}</td>
1141
+ renderTaskRow(task, index, isNested = false) {
1142
+ const lastExec = this._lastExecutionTimes.get(task.id);
1143
+ const isRecentlyExecuted = lastExec && Date.now() - lastExec < 1e3;
1144
+ const rowClass = isRecentlyExecuted ? "recently-executed" : "";
1145
+ const nestedClass = isNested ? "nested-task" : "";
1146
+ return `
1147
+ <tr data-id="${task.id}" class="${rowClass} ${nestedClass}">
1148
+ <td class="col-num">
1149
+ ${isNested ? "" : index + 1}
1150
+ </td>
875
1151
  <td class="col-id">
876
- <div class="task-id">${task.id}</div>
1152
+ <div class="task-id">
1153
+ ${task.id}
1154
+ ${task.namespace && task.namespace !== "default" && !isNested ? `<span class="namespace-badge" title="Namespace">${task.namespace}</span>` : ""}
1155
+ </div>
877
1156
  <div class="tags">
878
1157
  ${task.tags && task.tags.length > 0 ? task.tags.map((t2) => `<span class="tag">${t2}</span>`).join("") : `<span class="no-tags">${t("list.noTags")}</span>`}
879
1158
  </div>
@@ -892,7 +1171,38 @@ class TaskList extends HTMLElement {
892
1171
  </td>
893
1172
  </tr>
894
1173
  `;
895
- }).join("");
1174
+ }
1175
+ renderRows() {
1176
+ const tbody = this._shadow.querySelector("tbody");
1177
+ if (!tbody) return;
1178
+ const groups = this.groupTasksByNamespace(this._tasks);
1179
+ let html = "";
1180
+ let globalIndex = 0;
1181
+ const defaultTasks = groups.get("default");
1182
+ if (defaultTasks && defaultTasks.length > 0) {
1183
+ html += defaultTasks.map((task) => this.renderTaskRow(task, ++globalIndex, false)).join("");
1184
+ }
1185
+ groups.delete("default");
1186
+ const sortedNamespaces = Array.from(groups.keys()).sort((a, b) => a.localeCompare(b));
1187
+ sortedNamespaces.forEach((ns) => {
1188
+ const tasks = groups.get(ns);
1189
+ const isExpanded = this._expandedNamespaces.has(ns);
1190
+ const icon = isExpanded ? "▼" : "▶";
1191
+ html += `
1192
+ <tr class="namespace-row ${isExpanded ? "ns-expanded" : ""}" data-ns="${ns}">
1193
+ <td colspan="8">
1194
+ <span class="ns-toggle">${icon}</span>
1195
+ <span class="ns-icon">📂</span>
1196
+ <span class="ns-name">${ns}</span>
1197
+ <span class="ns-count">(${tasks.length})</span>
1198
+ </td>
1199
+ </tr>
1200
+ `;
1201
+ if (isExpanded) {
1202
+ html += tasks.map((task) => this.renderTaskRow(task, ++globalIndex, true)).join("");
1203
+ }
1204
+ });
1205
+ tbody.innerHTML = html;
896
1206
  }
897
1207
  render() {
898
1208
  this._shadow.innerHTML = `
@@ -974,6 +1284,18 @@ class TaskList extends HTMLElement {
974
1284
  .task-id {
975
1285
  font-weight: 600;
976
1286
  }
1287
+ .namespace-badge {
1288
+ display: inline-block;
1289
+ background: var(--hs-bg-secondary);
1290
+ color: var(--hs-text-secondary);
1291
+ border: 1px solid var(--hs-border);
1292
+ border-radius: 4px;
1293
+ padding: 0 4px;
1294
+ font-size: 9px;
1295
+ margin-left: 6px;
1296
+ font-family: monospace;
1297
+ vertical-align: middle;
1298
+ }
977
1299
  .tags {
978
1300
  display: flex;
979
1301
  gap: 4px;
@@ -1079,6 +1401,40 @@ class TaskList extends HTMLElement {
1079
1401
  color: var(--hs-text-secondary);
1080
1402
  border-color: var(--hs-border);
1081
1403
  }
1404
+ /* Namespace Styles */
1405
+ .namespace-row {
1406
+ background: var(--hs-bg-secondary);
1407
+ cursor: pointer;
1408
+ font-weight: 600;
1409
+ user-select: none;
1410
+ }
1411
+ .namespace-row:hover {
1412
+ background: var(--hs-border);
1413
+ }
1414
+ .namespace-row td {
1415
+ padding: 8px 12px;
1416
+ border-bottom: 1px solid var(--hs-border);
1417
+ }
1418
+ .ns-toggle {
1419
+ display: inline-block;
1420
+ width: 20px;
1421
+ text-align: center;
1422
+ font-size: 10px;
1423
+ color: var(--hs-text-secondary);
1424
+ transition: transform 0.2s;
1425
+ }
1426
+ .ns-icon {
1427
+ margin-right: 4px;
1428
+ }
1429
+ .ns-count {
1430
+ font-weight: normal;
1431
+ color: var(--hs-text-secondary);
1432
+ font-size: 11px;
1433
+ margin-left: 4px;
1434
+ }
1435
+ .nested-task .col-id {
1436
+ padding-left: 32px !important;
1437
+ }
1082
1438
  </style>
1083
1439
  <div class="table-container">
1084
1440
  <table>
@@ -1105,6 +1461,19 @@ class TaskList extends HTMLElement {
1105
1461
  `;
1106
1462
  this._shadow.addEventListener("click", (e) => {
1107
1463
  const target = e.target;
1464
+ const nsRow = target.closest(".namespace-row");
1465
+ if (nsRow) {
1466
+ const ns = nsRow.dataset.ns;
1467
+ if (ns) {
1468
+ if (this._expandedNamespaces.has(ns)) {
1469
+ this._expandedNamespaces.delete(ns);
1470
+ } else {
1471
+ this._expandedNamespaces.add(ns);
1472
+ }
1473
+ this.renderRows();
1474
+ }
1475
+ return;
1476
+ }
1108
1477
  const btn = target.closest("button");
1109
1478
  if (btn) {
1110
1479
  const action = btn.dataset.action;
@@ -1119,7 +1488,7 @@ class TaskList extends HTMLElement {
1119
1488
  return;
1120
1489
  }
1121
1490
  const tr = target.closest("tr");
1122
- if (tr && !target.closest(".col-actions")) {
1491
+ if (tr && !target.closest(".col-actions") && !tr.classList.contains("namespace-row")) {
1123
1492
  const id = tr.dataset.id;
1124
1493
  this.dispatchEvent(new CustomEvent("task-select", {
1125
1494
  detail: id,
@@ -1744,6 +2113,7 @@ class DevTools extends HTMLElement {
1744
2113
  __publicField(this, "$taskDetail");
1745
2114
  __publicField(this, "$timeline");
1746
2115
  __publicField(this, "$trigger");
2116
+ __publicField(this, "$overlay");
1747
2117
  this._shadow = this.attachShadow({ mode: "open" });
1748
2118
  this.store = new DevToolsStore();
1749
2119
  }
@@ -1857,6 +2227,7 @@ class DevTools extends HTMLElement {
1857
2227
  this.$taskDetail = this._shadow.querySelector("hs-task-detail");
1858
2228
  this.$timeline = this._shadow.querySelector("hs-timeline");
1859
2229
  this.$trigger = this._shadow.querySelector("hs-floating-trigger");
2230
+ this.$overlay = this._shadow.querySelector(".overlay");
1860
2231
  }
1861
2232
  bindStore() {
1862
2233
  try {
@@ -1872,6 +2243,7 @@ class DevTools extends HTMLElement {
1872
2243
  if (isOpen) {
1873
2244
  this.$panel.classList.add("open");
1874
2245
  this.$trigger.style.display = "none";
2246
+ this.$overlay.style.display = "block";
1875
2247
  if (pos === "right") {
1876
2248
  this.$panel.style.right = "0";
1877
2249
  } else {
@@ -1880,6 +2252,7 @@ class DevTools extends HTMLElement {
1880
2252
  } else {
1881
2253
  this.$panel.classList.remove("open");
1882
2254
  this.$trigger.style.display = "block";
2255
+ this.$overlay.style.display = "none";
1883
2256
  if (pos === "right") {
1884
2257
  this.$panel.style.right = `-${size.width}px`;
1885
2258
  } else {
@@ -2024,9 +2397,8 @@ class DevTools extends HTMLElement {
2024
2397
  const text = e.detail;
2025
2398
  this.store.setFilterText(text);
2026
2399
  });
2027
- this.addEventListener("resize", (e) => {
2028
- const size = e.detail;
2029
- this.store.setPanelSize(size);
2400
+ this.$overlay.addEventListener("click", () => {
2401
+ this.store.toggle();
2030
2402
  });
2031
2403
  this.$taskList.addEventListener("task-select", (e) => {
2032
2404
  const id = e.detail;
@@ -2078,6 +2450,16 @@ class DevTools extends HTMLElement {
2078
2450
  color: var(--hs-text);
2079
2451
  line-height: var(--hs-line-height);
2080
2452
  }
2453
+ .overlay {
2454
+ position: fixed;
2455
+ top: 0;
2456
+ left: 0;
2457
+ width: 100vw;
2458
+ height: 100vh;
2459
+ background: rgba(0, 0, 0, 0.5);
2460
+ z-index: calc(var(--hs-z-index) - 1); /* 确保在面板之下,但在应用之上 */
2461
+ display: none; /* 默认隐藏 */
2462
+ }
2081
2463
  .panel {
2082
2464
  position: fixed;
2083
2465
  background: var(--hs-bg);
@@ -2141,6 +2523,7 @@ class DevTools extends HTMLElement {
2141
2523
  }
2142
2524
  </style>
2143
2525
 
2526
+ <div class="overlay"></div>
2144
2527
  <hs-floating-trigger></hs-floating-trigger>
2145
2528
 
2146
2529
  <div class="panel dock-right">