hyper-scheduler 1.0.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 (76) hide show
  1. package/.editorconfig +21 -0
  2. package/.eslintrc.cjs +26 -0
  3. package/GEMINI.md +1 -0
  4. package/README.md +38 -0
  5. package/docs/.vitepress/config.ts +52 -0
  6. package/docs/README.md +120 -0
  7. package/docs/api/devtools.md +232 -0
  8. package/docs/api/index.md +178 -0
  9. package/docs/api/scheduler.md +322 -0
  10. package/docs/api/task.md +439 -0
  11. package/docs/api/types.md +365 -0
  12. package/docs/examples/index.md +295 -0
  13. package/docs/guide/best-practices.md +436 -0
  14. package/docs/guide/core-concepts.md +363 -0
  15. package/docs/guide/getting-started.md +138 -0
  16. package/docs/index.md +33 -0
  17. package/docs/public/logo.svg +54 -0
  18. package/examples/browser/index.html +354 -0
  19. package/examples/node/simple.js +36 -0
  20. package/examples/react-demo/index.html +12 -0
  21. package/examples/react-demo/package.json +23 -0
  22. package/examples/react-demo/src/App.css +212 -0
  23. package/examples/react-demo/src/App.jsx +160 -0
  24. package/examples/react-demo/src/main.jsx +9 -0
  25. package/examples/react-demo/vite.config.ts +12 -0
  26. package/examples/react-demo/yarn.lock +752 -0
  27. package/examples/vue-demo/index.html +12 -0
  28. package/examples/vue-demo/package.json +21 -0
  29. package/examples/vue-demo/src/App.vue +373 -0
  30. package/examples/vue-demo/src/main.ts +4 -0
  31. package/examples/vue-demo/vite.config.ts +13 -0
  32. package/examples/vue-demo/yarn.lock +375 -0
  33. package/package.json +51 -0
  34. package/src/constants.ts +18 -0
  35. package/src/core/retry-strategy.ts +28 -0
  36. package/src/core/scheduler.ts +601 -0
  37. package/src/core/task-registry.ts +58 -0
  38. package/src/index.ts +74 -0
  39. package/src/platform/browser/browser-timer.ts +66 -0
  40. package/src/platform/browser/main-thread-timer.ts +16 -0
  41. package/src/platform/browser/worker.ts +31 -0
  42. package/src/platform/node/debug-cli.ts +19 -0
  43. package/src/platform/node/node-timer.ts +15 -0
  44. package/src/platform/timer-strategy.ts +19 -0
  45. package/src/plugins/dev-tools.ts +101 -0
  46. package/src/types.ts +115 -0
  47. package/src/ui/components/devtools.ts +525 -0
  48. package/src/ui/components/floating-trigger.ts +102 -0
  49. package/src/ui/components/icons.ts +16 -0
  50. package/src/ui/components/resizer.ts +129 -0
  51. package/src/ui/components/task-detail.ts +228 -0
  52. package/src/ui/components/task-header.ts +319 -0
  53. package/src/ui/components/task-list.ts +416 -0
  54. package/src/ui/components/timeline.ts +364 -0
  55. package/src/ui/debug-panel.ts +56 -0
  56. package/src/ui/i18n/en.ts +76 -0
  57. package/src/ui/i18n/index.ts +42 -0
  58. package/src/ui/i18n/zh.ts +76 -0
  59. package/src/ui/store/dev-tools-store.ts +191 -0
  60. package/src/ui/styles/theme.css.ts +56 -0
  61. package/src/ui/styles.ts +43 -0
  62. package/src/utils/cron-lite.ts +221 -0
  63. package/src/utils/cron.ts +20 -0
  64. package/src/utils/id.ts +10 -0
  65. package/src/utils/schedule.ts +93 -0
  66. package/src/vite-env.d.ts +1 -0
  67. package/stats.html +4949 -0
  68. package/tests/integration/Debug.test.ts +58 -0
  69. package/tests/unit/Plugin.test.ts +16 -0
  70. package/tests/unit/RetryStrategy.test.ts +21 -0
  71. package/tests/unit/Scheduler.test.ts +38 -0
  72. package/tests/unit/schedule.test.ts +70 -0
  73. package/tests/unit/ui/DevToolsStore.test.ts +67 -0
  74. package/tsconfig.json +28 -0
  75. package/vite.config.ts +51 -0
  76. package/vitest.config.ts +24 -0
@@ -0,0 +1,129 @@
1
+ import { themeStyles } from '../styles/theme.css';
2
+
3
+ export class Resizer extends HTMLElement {
4
+ private _shadow: ShadowRoot;
5
+ private startX = 0;
6
+ private startY = 0;
7
+ private startWidth = 0;
8
+ private startHeight = 0;
9
+ private panel: HTMLElement | null = null;
10
+ private mode: 'right' | 'bottom' = 'right';
11
+
12
+ constructor() {
13
+ super();
14
+ this._shadow = this.attachShadow({ mode: 'open' });
15
+ }
16
+
17
+ // 检测是否为移动端
18
+ private isMobile(): boolean {
19
+ return window.innerWidth <= 480;
20
+ }
21
+
22
+ connectedCallback() {
23
+ this.render();
24
+ this.addEventListeners();
25
+ this.panel = this.closest('.panel') as HTMLElement;
26
+ if (this.panel && this.panel.classList.contains('dock-bottom')) {
27
+ this.mode = 'bottom';
28
+ }
29
+ }
30
+
31
+ private addEventListeners() {
32
+ const handle = this._shadow.querySelector('.handle') as HTMLElement;
33
+
34
+ const onMouseMove = (e: MouseEvent) => {
35
+ if (!this.panel || this.isMobile()) return;
36
+
37
+ if (this.mode === 'right') {
38
+ const dx = this.startX - e.clientX;
39
+ const newWidth = Math.max(300, Math.min(window.innerWidth - 50, this.startWidth + dx));
40
+ this.panel.style.width = `${newWidth}px`;
41
+ // Emit resize event for persistence
42
+ this.dispatchEvent(new CustomEvent('resize', { detail: { width: newWidth }, bubbles: true, composed: true }));
43
+ } else {
44
+ const newHeight = Math.max(200, Math.min(window.innerHeight - 50, this.startHeight + (this.startY - e.clientY)));
45
+ this.panel.style.height = `${newHeight}px`;
46
+ this.dispatchEvent(new CustomEvent('resize', { detail: { height: newHeight }, bubbles: true, composed: true }));
47
+ }
48
+ };
49
+
50
+ const onMouseUp = () => {
51
+ document.removeEventListener('mousemove', onMouseMove);
52
+ document.removeEventListener('mouseup', onMouseUp);
53
+ document.body.style.userSelect = '';
54
+ document.body.style.cursor = '';
55
+ };
56
+
57
+ handle.addEventListener('mousedown', (e) => {
58
+ // 移动端禁用拖拽
59
+ if (this.isMobile()) return;
60
+
61
+ this.startX = e.clientX;
62
+ this.startY = e.clientY;
63
+
64
+ if (this.panel) {
65
+ if (this.panel.classList.contains('dock-bottom')) {
66
+ this.mode = 'bottom';
67
+ this.startHeight = this.panel.offsetHeight;
68
+ } else {
69
+ this.mode = 'right';
70
+ this.startWidth = this.panel.offsetWidth;
71
+ }
72
+ }
73
+
74
+ document.addEventListener('mousemove', onMouseMove);
75
+ document.addEventListener('mouseup', onMouseUp);
76
+ document.body.style.userSelect = 'none';
77
+ document.body.style.cursor = this.mode === 'right' ? 'col-resize' : 'row-resize';
78
+ });
79
+ }
80
+
81
+ render() {
82
+ this._shadow.innerHTML = `
83
+ <style>
84
+ ${themeStyles}
85
+ :host {
86
+ display: block;
87
+ z-index: 100;
88
+ }
89
+ .handle {
90
+ background: transparent;
91
+ transition: background 0.2s;
92
+ }
93
+ .handle:hover {
94
+ background: var(--hs-primary);
95
+ }
96
+
97
+ /* Right Dock Mode (Vertical Handle on Left) */
98
+ :host-context(.dock-right) .handle {
99
+ width: 4px;
100
+ height: 100%;
101
+ cursor: col-resize;
102
+ position: absolute;
103
+ left: 0;
104
+ top: 0;
105
+ }
106
+
107
+ /* Bottom Dock Mode (Horizontal Handle on Top) */
108
+ :host-context(.dock-bottom) .handle {
109
+ width: 100%;
110
+ height: 4px;
111
+ cursor: row-resize;
112
+ position: absolute;
113
+ top: 0;
114
+ left: 0;
115
+ }
116
+
117
+ /* 移动端隐藏拖拽手柄 */
118
+ @media (max-width: 480px) {
119
+ .handle {
120
+ display: none;
121
+ }
122
+ }
123
+ </style>
124
+ <div class="handle"></div>
125
+ `;
126
+ }
127
+ }
128
+
129
+ customElements.define('hs-resizer', Resizer);
@@ -0,0 +1,228 @@
1
+ import { themeStyles } from '../styles/theme.css';
2
+ import { TaskSnapshot, ExecutionRecord } from '../../types';
3
+ import { t } from '../i18n';
4
+ import { ICONS } from './icons';
5
+
6
+ export class TaskDetail extends HTMLElement {
7
+ private _shadow: ShadowRoot;
8
+ private _task: TaskSnapshot | null = null;
9
+ private _history: ExecutionRecord[] = [];
10
+
11
+ constructor() {
12
+ super();
13
+ this._shadow = this.attachShadow({ mode: 'open' });
14
+ }
15
+
16
+ connectedCallback() {
17
+ this.render();
18
+ this._shadow.addEventListener('click', (e) => {
19
+ if ((e.target as Element).closest('.back-btn')) {
20
+ this.dispatchEvent(new CustomEvent('back'));
21
+ }
22
+ });
23
+ }
24
+
25
+ set task(task: TaskSnapshot | null) {
26
+ this._task = task;
27
+ this.renderContent();
28
+ }
29
+
30
+ set history(h: ExecutionRecord[]) {
31
+ this._history = h || [];
32
+ this.renderContent();
33
+ }
34
+
35
+ // Method to update texts when language changes
36
+ updateTexts() {
37
+ this.renderContent();
38
+ }
39
+
40
+ private renderContent() {
41
+ const container = this._shadow.querySelector('.content');
42
+ if (!container) return;
43
+
44
+ if (!this._task) {
45
+ container.innerHTML = `<div class="empty">${t('detail.noTask')}</div>`;
46
+ return;
47
+ }
48
+
49
+ const task = this._task;
50
+ const config = {
51
+ id: task.id,
52
+ schedule: task.schedule,
53
+ tags: task.tags,
54
+ };
55
+
56
+ // Calculate average duration
57
+ const avgDuration = this._history.length > 0
58
+ ? Math.round(this._history.reduce((sum, r) => sum + r.duration, 0) / this._history.length)
59
+ : 0;
60
+
61
+ container.innerHTML = `
62
+ <div class="header">
63
+ <button class="back-btn" title="${t('detail.back')}">${ICONS.back}</button>
64
+ <h2>📂 ${task.id}</h2>
65
+ </div>
66
+
67
+ <div class="section">
68
+ <div class="config-label">${t('detail.config')}:</div>
69
+ <pre>${JSON.stringify(config, null, 2)}</pre>
70
+ </div>
71
+
72
+ <div class="section">
73
+ <h3>📜 ${t('detail.history')} (${t('detail.lastRuns', { n: this._history.length })}) ${avgDuration > 0 ? `- ${t('detail.avgDuration')}: ${avgDuration}ms` : ''}</h3>
74
+ <table class="history-table">
75
+ <thead>
76
+ <tr>
77
+ <th>#</th>
78
+ <th>${t('detail.startTime')}</th>
79
+ <th>${t('detail.duration')}</th>
80
+ <th>${t('detail.drift')}</th>
81
+ <th>${t('detail.status')}</th>
82
+ </tr>
83
+ </thead>
84
+ <tbody>
85
+ ${this._history.length === 0 ? `<tr><td colspan="5" class="no-data">${t('detail.noHistory')}</td></tr>` : ''}
86
+ ${this._history.slice().reverse().map((run, idx) => {
87
+ const drift = 0; // TODO: calculate drift from expected vs actual time
88
+ const driftStr = drift > 0 ? `+${drift}ms` : drift < 0 ? `${drift}ms` : '0ms';
89
+ const statusIcon = run.success
90
+ ? `✅ ${t('detail.success')}`
91
+ : run.error
92
+ ? `❌ ${t('detail.error')}: ${run.error}`
93
+ : `⚠️ ${t('detail.failed')}`;
94
+ const durationClass = run.duration > 100 ? 'slow' : '';
95
+
96
+ return `
97
+ <tr class="${run.success ? 'success' : 'error'}">
98
+ <td class="col-num">${this._history.length - idx}</td>
99
+ <td>${new Date(run.timestamp).toLocaleTimeString('en-US', { hour12: false })}</td>
100
+ <td class="${durationClass}">${run.duration}ms</td>
101
+ <td>${driftStr}</td>
102
+ <td class="status-cell">${statusIcon}</td>
103
+ </tr>
104
+ `;
105
+ }).join('')}
106
+ </tbody>
107
+ </table>
108
+ </div>
109
+ `;
110
+ }
111
+
112
+ render() {
113
+ this._shadow.innerHTML = `
114
+ <style>
115
+ ${themeStyles}
116
+ :host {
117
+ display: block;
118
+ height: 100%;
119
+ background: var(--hs-bg);
120
+ overflow-y: auto;
121
+ }
122
+ .content {
123
+ padding: 16px;
124
+ }
125
+ .header {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: 16px;
129
+ margin-bottom: 24px;
130
+ padding-bottom: 16px;
131
+ border-bottom: 2px solid var(--hs-border);
132
+ }
133
+ .back-btn {
134
+ background: var(--hs-bg-secondary);
135
+ border: 1px solid var(--hs-border);
136
+ color: var(--hs-text);
137
+ padding: 6px 12px;
138
+ border-radius: 4px;
139
+ cursor: pointer;
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 4px;
143
+ font-size: 12px;
144
+ }
145
+ .back-btn:hover {
146
+ background: var(--hs-primary);
147
+ color: white;
148
+ border-color: var(--hs-primary);
149
+ }
150
+ h2 {
151
+ margin: 0;
152
+ font-size: 16px;
153
+ font-weight: 600;
154
+ }
155
+ h3 {
156
+ font-size: 13px;
157
+ color: var(--hs-text);
158
+ margin-bottom: 12px;
159
+ font-weight: 600;
160
+ }
161
+ .section {
162
+ margin-bottom: 32px;
163
+ }
164
+ .config-label {
165
+ font-size: 12px;
166
+ color: var(--hs-text-secondary);
167
+ margin-bottom: 8px;
168
+ }
169
+ pre {
170
+ background: var(--hs-bg-secondary);
171
+ padding: 12px;
172
+ border-radius: 6px;
173
+ border: 1px solid var(--hs-border);
174
+ font-family: 'Monaco', 'Menlo', monospace;
175
+ font-size: 11px;
176
+ overflow-x: auto;
177
+ line-height: 1.5;
178
+ }
179
+ .history-table {
180
+ width: 100%;
181
+ border-collapse: collapse;
182
+ font-size: 11px;
183
+ }
184
+ .history-table th {
185
+ text-align: left;
186
+ padding: 8px;
187
+ border-bottom: 2px solid var(--hs-border);
188
+ color: var(--hs-text-secondary);
189
+ font-weight: 600;
190
+ background: var(--hs-bg-secondary);
191
+ }
192
+ .history-table td {
193
+ padding: 8px;
194
+ border-bottom: 1px solid var(--hs-border);
195
+ }
196
+ .history-table tr.success {
197
+ background: rgba(34, 197, 94, 0.05);
198
+ }
199
+ .history-table tr.error {
200
+ background: rgba(239, 68, 68, 0.05);
201
+ }
202
+ .history-table tr:hover {
203
+ background: var(--hs-bg-secondary);
204
+ }
205
+ .col-num {
206
+ width: 40px;
207
+ color: var(--hs-text-secondary);
208
+ }
209
+ .slow {
210
+ color: var(--hs-warning);
211
+ font-weight: 600;
212
+ }
213
+ .status-cell {
214
+ font-size: 11px;
215
+ }
216
+ .no-data {
217
+ color: var(--hs-text-secondary);
218
+ font-style: italic;
219
+ text-align: center;
220
+ padding: 24px;
221
+ }
222
+ </style>
223
+ <div class="content"></div>
224
+ `;
225
+ }
226
+ }
227
+
228
+ customElements.define('hs-task-detail', TaskDetail);
@@ -0,0 +1,319 @@
1
+ import { themeStyles } from '../styles/theme.css';
2
+ import { ICONS } from './icons';
3
+ import { t } from '../i18n';
4
+
5
+ export class TaskHeader extends HTMLElement {
6
+ private _shadow: ShadowRoot;
7
+ private _fps: number = 0;
8
+ private _stats: { active: number; total: number } = { active: 0, total: 0 };
9
+ private _theme: 'light' | 'dark' | 'auto' = 'auto';
10
+ private _activeTab: 'tasks' | 'timeline' = 'tasks';
11
+ private _language: 'en' | 'zh' = 'en';
12
+ private _schedulerRunning: boolean = false;
13
+
14
+ private $fps!: HTMLElement;
15
+ private $stats!: HTMLElement;
16
+ private $schedulerStatus!: HTMLElement;
17
+ private $themeIcon!: HTMLElement;
18
+ private $dockIcon!: HTMLElement;
19
+ private $tabs!: NodeListOf<HTMLElement>;
20
+ private $searchInput!: HTMLInputElement;
21
+ private $title!: HTMLElement;
22
+ private $langBtn!: HTMLElement;
23
+
24
+ constructor() {
25
+ super();
26
+ this._shadow = this.attachShadow({ mode: 'open' });
27
+ }
28
+
29
+ connectedCallback() {
30
+ this.render();
31
+ this.cacheDom();
32
+ this.addEventListeners();
33
+ this.updateView();
34
+ }
35
+
36
+ set fps(val: number) {
37
+ this._fps = Math.round(val);
38
+ if (this.$fps) {
39
+ const color = this._fps < 30 ? 'var(--hs-danger)' : (this._fps < 50 ? 'var(--hs-warning)' : 'var(--hs-success)');
40
+ this.$fps.innerHTML = `⚡ ${t('stats.fps')}: <span style="color:${color}">${this._fps}</span> (${t('stats.mainThread')})`;
41
+ }
42
+ }
43
+
44
+ set stats(val: { active: number; total: number }) {
45
+ this._stats = val;
46
+ if (this.$stats) {
47
+ this.$stats.innerHTML = `📊 ${t('stats.status')}: <span style="color:var(--hs-success)">🟢 ${t('stats.active')}: ${val.active}</span> <span style="margin-left:12px;color:var(--hs-text-secondary)">⚪ ${t('stats.total')}: ${val.total}</span>`;
48
+ }
49
+ }
50
+
51
+ set schedulerRunning(val: boolean) {
52
+ this._schedulerRunning = val;
53
+ if (this.$schedulerStatus) {
54
+ const statusText = val ? t('stats.running') : t('stats.stopped');
55
+ const statusColor = val ? 'var(--hs-success)' : 'var(--hs-danger)';
56
+ const statusIcon = val ? '▶️' : '⏹️';
57
+ this.$schedulerStatus.innerHTML = `${statusIcon} ${t('stats.scheduler')}: <span style="color:${statusColor}">${statusText}</span>`;
58
+ }
59
+ }
60
+
61
+ set theme(val: 'light' | 'dark' | 'auto') {
62
+ this._theme = val;
63
+ this.setAttribute('theme', val);
64
+ this.updateThemeIcon();
65
+ }
66
+
67
+ set language(val: 'en' | 'zh') {
68
+ this._language = val;
69
+ this.updateTexts();
70
+ }
71
+
72
+ set dockPosition(val: 'right' | 'bottom') {
73
+ if (this.$dockIcon) {
74
+ // 显示目标位置的图标(点击后切换到的位置)
75
+ // right -> 显示 dock (底部图标),表示可以切换到底部
76
+ // bottom -> 显示 dockRight (右侧图标),表示可以切换到右侧
77
+ this.$dockIcon.innerHTML = val === 'right' ? ICONS.dock : ICONS.dockRight;
78
+ this.$dockIcon.parentElement?.setAttribute('title', t('header.toggleDock'));
79
+ }
80
+ }
81
+
82
+ set activeTab(val: 'tasks' | 'timeline') {
83
+ this._activeTab = val;
84
+ this.updateTabs();
85
+ }
86
+
87
+ private cacheDom() {
88
+ this.$fps = this._shadow.querySelector('.fps')!;
89
+ this.$stats = this._shadow.querySelector('.stats')!;
90
+ this.$schedulerStatus = this._shadow.querySelector('.scheduler-status')!;
91
+ this.$themeIcon = this._shadow.querySelector('.theme-btn span')!;
92
+ this.$dockIcon = this._shadow.querySelector('.dock-btn')!;
93
+ this.$tabs = this._shadow.querySelectorAll('.tab');
94
+ this.$searchInput = this._shadow.querySelector('.search-input')!;
95
+ this.$title = this._shadow.querySelector('.title')!;
96
+ this.$langBtn = this._shadow.querySelector('.lang-btn')!;
97
+ }
98
+
99
+ private addEventListeners() {
100
+ this._shadow.querySelector('.dock-btn')?.addEventListener('click', () => {
101
+ this.dispatchEvent(new CustomEvent('dock-toggle'));
102
+ });
103
+
104
+ this._shadow.querySelector('.theme-btn')?.addEventListener('click', () => {
105
+ const newTheme = this._theme === 'dark' ? 'light' : 'dark';
106
+ this.dispatchEvent(new CustomEvent('theme-toggle', { detail: newTheme }));
107
+ });
108
+
109
+ this._shadow.querySelector('.lang-btn')?.addEventListener('click', () => {
110
+ const newLang = this._language === 'en' ? 'zh' : 'en';
111
+ this.dispatchEvent(new CustomEvent('lang-toggle', { detail: newLang }));
112
+ });
113
+
114
+ this._shadow.querySelector('.close-btn')?.addEventListener('click', () => {
115
+ this.dispatchEvent(new CustomEvent('close'));
116
+ });
117
+
118
+ this.$tabs.forEach(tab => {
119
+ tab.addEventListener('click', (e) => {
120
+ const target = (e.currentTarget as HTMLElement).dataset.tab;
121
+ this.dispatchEvent(new CustomEvent('tab-change', { detail: target }));
122
+ });
123
+ });
124
+
125
+ this.$searchInput?.addEventListener('input', (e) => {
126
+ const val = (e.target as HTMLInputElement).value;
127
+ this.dispatchEvent(new CustomEvent('search', { detail: val }));
128
+ });
129
+ }
130
+
131
+ private updateThemeIcon() {
132
+ if (this.$themeIcon) {
133
+ this.$themeIcon.innerHTML = this._theme === 'dark' ? ICONS.moon : ICONS.sun;
134
+ }
135
+ }
136
+
137
+ private updateTabs() {
138
+ this.$tabs.forEach(tab => {
139
+ if (tab.dataset.tab === this._activeTab) {
140
+ tab.classList.add('active');
141
+ } else {
142
+ tab.classList.remove('active');
143
+ }
144
+ });
145
+ }
146
+
147
+ private updateTexts() {
148
+ if (this.$title) this.$title.innerHTML = `🕒 ${t('header.title')}`;
149
+ if (this.$searchInput) this.$searchInput.placeholder = t('header.searchPlaceholder');
150
+ // 显示目标语言(点击后切换到的语言)
151
+ if (this.$langBtn) this.$langBtn.textContent = this._language === 'en' ? '中' : 'EN';
152
+
153
+ this.$tabs.forEach(tab => {
154
+ const key = tab.dataset.tab;
155
+ if (key === 'tasks') tab.innerHTML = `📌 ${t('tabs.tasks')}`;
156
+ if (key === 'timeline') tab.innerHTML = `📈 ${t('tabs.timeline')}`;
157
+ });
158
+
159
+ // Force update stats, fps, and scheduler status to refresh labels
160
+ this.stats = this._stats;
161
+ this.fps = this._fps;
162
+ this.schedulerRunning = this._schedulerRunning;
163
+ }
164
+
165
+ private updateView() {
166
+ this.updateThemeIcon();
167
+ this.updateTabs();
168
+ this.updateTexts();
169
+ }
170
+
171
+ render() {
172
+ this._shadow.innerHTML = `
173
+ <style>
174
+ ${themeStyles}
175
+ :host {
176
+ display: block;
177
+ background: var(--hs-bg);
178
+ border-bottom: 1px solid var(--hs-border);
179
+ padding: 0 16px;
180
+ height: var(--hs-header-height);
181
+ height: auto;
182
+ }
183
+ .top-bar {
184
+ display: flex;
185
+ justify-content: space-between;
186
+ align-items: center;
187
+ height: 40px;
188
+ border-bottom: 1px solid var(--hs-border);
189
+ }
190
+ .title {
191
+ font-weight: 600;
192
+ font-size: 13px;
193
+ color: var(--hs-text);
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 6px;
197
+ }
198
+ .search-box {
199
+ flex: 1;
200
+ max-width: 300px;
201
+ margin: 0px 32px 0 16px;
202
+ }
203
+ .search-input {
204
+ width: 100%;
205
+ background: var(--hs-bg-secondary);
206
+ border: 1px solid var(--hs-border);
207
+ color: var(--hs-text);
208
+ padding: 6px 12px;
209
+ border-radius: 4px;
210
+ font-size: 12px;
211
+ }
212
+ .search-input::placeholder {
213
+ color: var(--hs-text-secondary);
214
+ }
215
+ .controls {
216
+ display: flex;
217
+ align-items: center;
218
+ gap: 4px;
219
+ }
220
+ button {
221
+ background: transparent;
222
+ border: none;
223
+ color: var(--hs-text-secondary);
224
+ cursor: pointer;
225
+ padding: 4px;
226
+ border-radius: 4px;
227
+ display: flex;
228
+ align-items: center;
229
+ justify-content: center;
230
+ font-size: 12px;
231
+ width: 28px;
232
+ height: 28px;
233
+ }
234
+ .theme-btn span {
235
+ margin-top: 4px;
236
+ }
237
+ button:hover {
238
+ background: var(--hs-bg-secondary);
239
+ color: var(--hs-text);
240
+ }
241
+ button svg {
242
+ width: 16px;
243
+ height: 16px;
244
+ }
245
+ .lang-btn {
246
+ font-weight: 600;
247
+ }
248
+ .stats-bar {
249
+ display: flex;
250
+ justify-content: space-between;
251
+ align-items: center;
252
+ height: 30px;
253
+ font-size: 11px;
254
+ color: var(--hs-text-secondary);
255
+ border-bottom: 1px solid var(--hs-border);
256
+ gap: 16px;
257
+ }
258
+ .stats-left {
259
+ display: flex;
260
+ gap: 16px;
261
+ }
262
+ .tabs-bar {
263
+ display: flex;
264
+ height: 36px;
265
+ gap: 0;
266
+ }
267
+ .tab {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 6px;
271
+ font-size: 12px;
272
+ color: var(--hs-text-secondary);
273
+ cursor: pointer;
274
+ border-bottom: 3px solid transparent;
275
+ padding: 0 16px;
276
+ transition: all 0.2s;
277
+ }
278
+ .tab:hover {
279
+ color: var(--hs-text);
280
+ background: var(--hs-bg-secondary);
281
+ }
282
+ .tab.active {
283
+ color: var(--hs-text);
284
+ font-weight: 600;
285
+ border-bottom-color: var(--hs-primary);
286
+ background: var(--hs-bg-secondary);
287
+ }
288
+ </style>
289
+
290
+ <div class="top-bar">
291
+ <div class="title"></div>
292
+ <div class="search-box">
293
+ <input type="text" class="search-input">
294
+ </div>
295
+ <div class="controls">
296
+ <button class="lang-btn" title="Switch Language">EN</button>
297
+ <button class="dock-btn" title="Toggle Dock">${ICONS.dock}</button>
298
+ <button class="theme-btn" title="Toggle Theme"><span>${ICONS.sun}</span></button>
299
+ <button class="close-btn" title="Close">${ICONS.close}</button>
300
+ </div>
301
+ </div>
302
+
303
+ <div class="stats-bar">
304
+ <div class="stats-left">
305
+ <div class="scheduler-status"></div>
306
+ <div class="stats"></div>
307
+ </div>
308
+ <div class="fps"></div>
309
+ </div>
310
+
311
+ <div class="tabs-bar">
312
+ <div class="tab active" data-tab="tasks"></div>
313
+ <div class="tab" data-tab="timeline"></div>
314
+ </div>
315
+ `;
316
+ }
317
+ }
318
+
319
+ customElements.define('hs-task-header', TaskHeader);