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,525 @@
1
+ import { DevToolsStore } from '../store/dev-tools-store';
2
+ import { SchedulerIntrospectionAPI, TaskControlAPI } from '../../types';
3
+ import { SchedulerEvents } from '../../constants';
4
+ import { themeStyles } from '../styles/theme.css';
5
+ import './floating-trigger';
6
+ import './task-header';
7
+ import './task-list';
8
+ import './task-detail';
9
+ import './timeline';
10
+ import './resizer';
11
+ import { TaskHeader } from './task-header';
12
+ import { TaskList } from './task-list';
13
+ import { TaskDetail } from './task-detail';
14
+ import { Timeline } from './timeline';
15
+
16
+ export class DevTools extends HTMLElement {
17
+ private _shadow: ShadowRoot;
18
+ private store: DevToolsStore;
19
+ private scheduler?: SchedulerIntrospectionAPI & TaskControlAPI;
20
+ private rAFId?: number;
21
+ private lastTime = 0;
22
+
23
+ private $panel!: HTMLElement;
24
+ private $header!: TaskHeader;
25
+ private $taskList!: TaskList;
26
+ private $taskDetail!: TaskDetail;
27
+ private $timeline!: Timeline;
28
+ private $trigger!: HTMLElement;
29
+
30
+ constructor() {
31
+ super();
32
+ this._shadow = this.attachShadow({ mode: 'open' });
33
+ this.store = new DevToolsStore();
34
+ }
35
+
36
+ connectedCallback() {
37
+ // IMPORTANT: Set language BEFORE rendering child components
38
+ // so that t() function returns correct translations during initial render
39
+ const languageAttr = this.getAttribute('language');
40
+ if (languageAttr === 'en' || languageAttr === 'zh') {
41
+ // Synchronously set language before any child component renders
42
+ this.store.setLanguageSync(languageAttr);
43
+ }
44
+
45
+ this.render();
46
+ this.cacheDom();
47
+ this.bindStore();
48
+
49
+ // Apply initial options from attributes (after bindStore so listeners are set up)
50
+ const dockAttr = this.getAttribute('dock');
51
+ console.log('[DevTools] dock attribute:', dockAttr);
52
+ if (dockAttr === 'bottom') {
53
+ this.store.setDockPosition('bottom');
54
+ }
55
+
56
+ const themeAttr = this.getAttribute('theme');
57
+ if (themeAttr === 'light' || themeAttr === 'dark' || themeAttr === 'auto') {
58
+ this.store.setTheme(themeAttr);
59
+ }
60
+
61
+ // Set language again through store to trigger UI updates for already rendered components
62
+ if (languageAttr === 'en' || languageAttr === 'zh') {
63
+ this.store.setLanguage(languageAttr);
64
+ }
65
+
66
+ // Apply trigger button options
67
+ const triggerBg = this.getAttribute('trigger-bg');
68
+ const triggerColor = this.getAttribute('trigger-color');
69
+ const triggerPosition = this.getAttribute('trigger-position');
70
+ console.log('[DevTools] trigger attrs:', { triggerBg, triggerColor, triggerPosition });
71
+ if (triggerBg) this.$trigger.setAttribute('bg-color', triggerBg);
72
+ if (triggerColor) this.$trigger.setAttribute('text-color', triggerColor);
73
+ if (triggerPosition) this.$trigger.setAttribute('position', triggerPosition);
74
+
75
+ // Apply default zoom to timeline
76
+ const defaultZoom = this.getAttribute('default-zoom');
77
+ if (defaultZoom) {
78
+ const zoom = parseFloat(defaultZoom);
79
+ if (!isNaN(zoom) && zoom >= 0.5 && zoom <= 5) {
80
+ this.$timeline.defaultZoom = zoom;
81
+ }
82
+ }
83
+
84
+ this.addEventListeners();
85
+ this.startLoop();
86
+ }
87
+
88
+ disconnectedCallback() {
89
+ if (this.rAFId) cancelAnimationFrame(this.rAFId);
90
+ }
91
+
92
+ setScheduler(api: SchedulerIntrospectionAPI & TaskControlAPI) {
93
+ this.scheduler = api;
94
+ this.store.setScheduler(api);
95
+
96
+ // Initial load - fetch immediately
97
+ const tasks = this.scheduler.getTasks();
98
+ tasks.forEach(t => this.store.updateTask(t));
99
+
100
+ // Get initial scheduler running state
101
+ const isRunning = this.scheduler.isRunning();
102
+ this.store.setSchedulerRunning(isRunning);
103
+
104
+ // Subscribe to ALL events and refresh task list
105
+ const refreshTasks = () => {
106
+ const allTasks = this.scheduler?.getTasks() || [];
107
+ allTasks.forEach(t => this.store.updateTask(t));
108
+ };
109
+
110
+ this.scheduler.on(SchedulerEvents.TASK_REGISTERED, refreshTasks);
111
+ this.scheduler.on(SchedulerEvents.TASK_UPDATED, (payload: any) => {
112
+ console.log('[DevTools] task_updated event:', payload);
113
+ refreshTasks();
114
+ });
115
+ this.scheduler.on(SchedulerEvents.TASK_STARTED, refreshTasks);
116
+ this.scheduler.on(SchedulerEvents.TASK_REMOVED, refreshTasks);
117
+ this.scheduler.on(SchedulerEvents.TASK_STOPPED, (payload: any) => {
118
+ console.log('[DevTools] task_stopped event:', payload);
119
+ refreshTasks();
120
+ });
121
+
122
+ // Listen to scheduler start/stop events
123
+ this.scheduler.on(SchedulerEvents.SCHEDULER_STARTED, () => {
124
+ console.log('[DevTools] scheduler_started event');
125
+ this.store.setSchedulerRunning(true);
126
+ refreshTasks();
127
+ });
128
+ this.scheduler.on(SchedulerEvents.SCHEDULER_STOPPED, () => {
129
+ console.log('[DevTools] scheduler_stopped event');
130
+ this.store.setSchedulerRunning(false);
131
+ refreshTasks();
132
+ });
133
+
134
+ this.scheduler.on(SchedulerEvents.TASK_COMPLETED, (payload: any) => {
135
+ refreshTasks();
136
+
137
+ // Add to history
138
+ if (payload && payload.taskId) {
139
+ this.store.addHistory(payload.taskId, {
140
+ timestamp: payload.task?.lastRun || Date.now(),
141
+ duration: payload.duration || 0,
142
+ success: true,
143
+ error: null
144
+ });
145
+ }
146
+ });
147
+
148
+ this.scheduler.on(SchedulerEvents.TASK_FAILED, (payload: any) => {
149
+ refreshTasks();
150
+
151
+ // Add to history
152
+ if (payload && payload.taskId) {
153
+ this.store.addHistory(payload.taskId, {
154
+ timestamp: payload.task?.lastRun || Date.now(),
155
+ duration: payload.duration || 0,
156
+ success: false,
157
+ error: payload.error || 'Unknown error'
158
+ });
159
+ }
160
+ });
161
+
162
+ // Add polling for real-time updates (fallback for missed events)
163
+ // Poll every 500ms when DevTools is open
164
+ setInterval(() => {
165
+ if (this.store.getState().isOpen) {
166
+ refreshTasks();
167
+ }
168
+ }, 500);
169
+ }
170
+
171
+ private cacheDom() {
172
+ this.$panel = this._shadow.querySelector('.panel')!;
173
+ this.$header = this._shadow.querySelector('hs-task-header') as TaskHeader;
174
+ this.$taskList = this._shadow.querySelector('hs-task-list') as TaskList;
175
+ this.$taskDetail = this._shadow.querySelector('hs-task-detail') as TaskDetail;
176
+ this.$timeline = this._shadow.querySelector('hs-timeline') as Timeline;
177
+ this.$trigger = this._shadow.querySelector('hs-floating-trigger')!;
178
+ }
179
+
180
+ private bindStore() {
181
+ // Restore size
182
+ try {
183
+ const saved = localStorage.getItem('hs-panel-size');
184
+ if (saved) {
185
+ this.store.setPanelSize(JSON.parse(saved));
186
+ }
187
+ } catch (e) { /* ignore */ }
188
+
189
+ // Bind Store -> UI
190
+ this.store.subscribe('isOpen', (isOpen) => {
191
+ const pos = this.store.getState().dockPosition;
192
+ const size = this.store.getState().panelSize;
193
+
194
+ if (isOpen) {
195
+ this.$panel.classList.add('open');
196
+ this.$trigger.style.display = 'none';
197
+ // Ensure panel is visible
198
+ if (pos === 'right') {
199
+ this.$panel.style.right = '0';
200
+ } else {
201
+ this.$panel.style.bottom = '0';
202
+ }
203
+ } else {
204
+ this.$panel.classList.remove('open');
205
+ this.$trigger.style.display = 'block';
206
+ // Ensure panel is hidden
207
+ if (pos === 'right') {
208
+ this.$panel.style.right = `-${size.width}px`;
209
+ } else {
210
+ this.$panel.style.bottom = `-${size.height}px`;
211
+ }
212
+ }
213
+ });
214
+
215
+ this.store.subscribe('theme', (theme) => {
216
+ const actualTheme = theme === 'auto' ? 'light' : theme;
217
+ this.setAttribute('theme', actualTheme);
218
+
219
+ // Propagate theme to all child components
220
+ this.$header.setAttribute('theme', actualTheme);
221
+ this.$taskList.setAttribute('theme', actualTheme);
222
+ this.$taskDetail.setAttribute('theme', actualTheme);
223
+ this.$timeline.setAttribute('theme', actualTheme);
224
+
225
+ this.$header.theme = theme;
226
+ });
227
+
228
+ this.store.subscribe('tasks', (tasks) => {
229
+ this.$taskList.tasks = tasks;
230
+ this.$timeline.data = { tasks, history: this.store.getState().history };
231
+
232
+ // Update stats - active = idle + running (正在调度或执行中的任务)
233
+ let active = 0;
234
+ tasks.forEach(t => {
235
+ if (t.status === 'idle' || t.status === 'running') active++;
236
+ });
237
+ this.$header.stats = { active, total: tasks.size };
238
+
239
+ // Update detail view if selected
240
+ const selectedId = this.store.getState().selectedTaskId;
241
+ if (selectedId && tasks.has(selectedId)) {
242
+ this.$taskDetail.task = tasks.get(selectedId) || null;
243
+ }
244
+ });
245
+
246
+ this.store.subscribe('history', (map) => {
247
+ this.$timeline.data = { tasks: this.store.getState().tasks, history: map };
248
+ const id = this.store.getState().selectedTaskId;
249
+ if (id) {
250
+ this.$taskDetail.history = map.get(id) || [];
251
+ }
252
+ });
253
+
254
+ this.store.subscribe('selectedTaskId', (id) => {
255
+ // Only affects tasks tab
256
+ if (this.store.getState().activeTab !== 'tasks') return;
257
+
258
+ if (id) {
259
+ this.$taskList.style.display = 'none';
260
+ this.$taskDetail.style.display = 'block';
261
+ const task = this.store.getState().tasks.get(id);
262
+ const history = this.store.getState().history.get(id);
263
+ this.$taskDetail.task = task || null;
264
+ this.$taskDetail.history = history || [];
265
+ } else {
266
+ this.$taskList.style.display = 'block';
267
+ this.$taskDetail.style.display = 'none';
268
+ }
269
+ });
270
+
271
+ this.store.subscribe('activeTab', (tab) => {
272
+ this.$header.activeTab = tab;
273
+ // Switch view logic later (Phase 5)
274
+ if (tab === 'tasks') {
275
+ this.$taskList.style.display = 'block';
276
+ this.$timeline.style.display = 'none';
277
+ // If selected, list is hidden?
278
+ if (this.store.getState().selectedTaskId) {
279
+ this.$taskList.style.display = 'none';
280
+ this.$taskDetail.style.display = 'block';
281
+ } else {
282
+ this.$taskList.style.display = 'block';
283
+ this.$taskDetail.style.display = 'none';
284
+ }
285
+ } else {
286
+ this.$taskList.style.display = 'none';
287
+ this.$taskDetail.style.display = 'none';
288
+ this.$timeline.style.display = 'block';
289
+ }
290
+ });
291
+
292
+ this.store.subscribe('dockPosition', (pos) => {
293
+ this.$header.dockPosition = pos;
294
+ const size = this.store.getState().panelSize;
295
+ const isOpen = this.store.getState().isOpen;
296
+ const isMobile = window.innerWidth <= 480;
297
+
298
+ if (pos === 'right') {
299
+ this.$panel.classList.add('dock-right');
300
+ this.$panel.classList.remove('dock-bottom');
301
+ // 移动端使用 100vw,桌面端使用自定义宽度
302
+ const width = isMobile ? window.innerWidth : size.width;
303
+ this.$panel.style.width = isMobile ? '100vw' : `${width}px`;
304
+ this.$panel.style.height = '100vh';
305
+ this.$panel.style.bottom = '';
306
+ this.$panel.style.right = isOpen ? '0' : `-${width}px`;
307
+ } else {
308
+ this.$panel.classList.add('dock-bottom');
309
+ this.$panel.classList.remove('dock-right');
310
+ this.$panel.style.width = '100%';
311
+ // 移动端使用固定 50vh,桌面端使用自定义高度
312
+ const height = isMobile ? '50vh' : `${size.height}px`;
313
+ this.$panel.style.height = height;
314
+ this.$panel.style.right = '';
315
+ this.$panel.style.bottom = isOpen ? '0' : (isMobile ? '-50vh' : `-${size.height}px`);
316
+ }
317
+ });
318
+
319
+ this.store.subscribe('panelSize', (size) => {
320
+ const pos = this.store.getState().dockPosition;
321
+ if (pos === 'right' && size.width) {
322
+ this.$panel.style.width = `${size.width}px`;
323
+ // Reset right position to ensure close animation works
324
+ this.$panel.style.right = this.store.getState().isOpen ? '0' : `-${size.width}px`;
325
+ } else if (pos === 'bottom' && size.height) {
326
+ this.$panel.style.height = `${size.height}px`;
327
+ // Reset bottom position to ensure close animation works
328
+ this.$panel.style.bottom = this.store.getState().isOpen ? '0' : `-${size.height}px`;
329
+ }
330
+ });
331
+
332
+ this.store.subscribe('language', (lang) => {
333
+ this.$header.language = lang;
334
+ // Update table headers for i18n
335
+ this.$taskList.updateHeaders();
336
+ // Update detail view if visible
337
+ this.$taskDetail.updateTexts?.();
338
+ // Update timeline if visible
339
+ this.$timeline.updateTexts?.();
340
+ });
341
+
342
+ this.store.subscribe('filterText', (text) => {
343
+ const tasks = this.store.getState().tasks;
344
+ this.$taskList.filter(text, tasks);
345
+ });
346
+
347
+ this.store.subscribe('schedulerRunning', (running) => {
348
+ this.$header.schedulerRunning = running;
349
+ });
350
+ }
351
+
352
+ private addEventListeners() {
353
+ this.$trigger.addEventListener('toggle', () => {
354
+ this.store.toggle();
355
+ });
356
+
357
+ this.$header.addEventListener('close', (e) => {
358
+ e.stopPropagation();
359
+ this.store.toggle();
360
+ });
361
+
362
+ this.$header.addEventListener('dock-toggle', () => {
363
+ const current = this.store.getState().dockPosition;
364
+ this.store.setDockPosition(current === 'right' ? 'bottom' : 'right');
365
+ });
366
+
367
+ this.$header.addEventListener('theme-toggle', (e: Event) => {
368
+ const theme = (e as CustomEvent).detail;
369
+ this.store.setTheme(theme);
370
+ });
371
+
372
+ this.$header.addEventListener('lang-toggle', (e: Event) => {
373
+ const lang = (e as CustomEvent).detail;
374
+ this.store.setLanguage(lang);
375
+ });
376
+
377
+ this.$header.addEventListener('tab-change', (e: Event) => {
378
+ const tab = (e as CustomEvent).detail;
379
+ this.store.setTab(tab);
380
+ });
381
+
382
+ this.$header.addEventListener('search', (e: Event) => {
383
+ const text = (e as CustomEvent).detail;
384
+ this.store.setFilterText(text);
385
+ });
386
+
387
+ // Listen to resize events from hs-resizer
388
+ this.addEventListener('resize', (e: Event) => {
389
+ const size = (e as CustomEvent).detail;
390
+ this.store.setPanelSize(size);
391
+ });
392
+
393
+ this.$taskList.addEventListener('task-select', (e: Event) => {
394
+ const id = (e as CustomEvent).detail;
395
+ this.store.selectTask(id);
396
+ });
397
+
398
+ this.$taskDetail.addEventListener('back', () => {
399
+ this.store.selectTask(null);
400
+ });
401
+
402
+ this.$taskList.addEventListener('task-action', (e: Event) => {
403
+ const { action, id } = (e as CustomEvent).detail;
404
+ console.log('[DevTools] task-action:', action, id);
405
+ switch (action) {
406
+ case 'trigger':
407
+ this.store.triggerTask(id);
408
+ break;
409
+ case 'stop':
410
+ this.store.stopTask(id);
411
+ break;
412
+ case 'start':
413
+ this.store.startTask(id);
414
+ break;
415
+ case 'remove':
416
+ if (confirm(`Remove task "${id}"?`)) {
417
+ this.store.removeTask(id);
418
+ }
419
+ break;
420
+ }
421
+ });
422
+ }
423
+
424
+ private startLoop() {
425
+ const loop = (time: number) => {
426
+ const delta = time - this.lastTime;
427
+ this.lastTime = time;
428
+ const fps = 1000 / delta;
429
+ if (this.$header) {
430
+ this.$header.fps = fps;
431
+ }
432
+ this.rAFId = requestAnimationFrame(loop);
433
+ };
434
+ this.rAFId = requestAnimationFrame(loop);
435
+ }
436
+
437
+ render() {
438
+ this._shadow.innerHTML = `
439
+ <style>
440
+ ${themeStyles}
441
+ :host {
442
+ font-family: var(--hs-font-family);
443
+ font-size: var(--hs-font-size);
444
+ color: var(--hs-text);
445
+ line-height: var(--hs-line-height);
446
+ }
447
+ .panel {
448
+ position: fixed;
449
+ background: var(--hs-bg);
450
+ box-shadow: var(--hs-shadow);
451
+ z-index: var(--hs-z-index);
452
+ transition: all 0.3s ease;
453
+ display: flex;
454
+ flex-direction: column;
455
+ border-left: 1px solid var(--hs-border);
456
+ }
457
+ /* Default Right Dock */
458
+ .panel.dock-right {
459
+ top: 0;
460
+ right: -500px;
461
+ width: 500px;
462
+ height: 100vh;
463
+ border-left: 1px solid var(--hs-border);
464
+ border-top: none;
465
+ }
466
+
467
+ /* Bottom Dock */
468
+ .panel.dock-bottom {
469
+ bottom: -50vh;
470
+ left: 0;
471
+ width: 100%;
472
+ height: 50vh;
473
+ max-height: 50vh;
474
+ border-top: 1px solid var(--hs-border);
475
+ border-left: none;
476
+ }
477
+
478
+ .content {
479
+ flex: 1;
480
+ overflow: hidden;
481
+ position: relative;
482
+ display: flex;
483
+ flex-direction: column;
484
+ }
485
+ .content > * {
486
+ flex: 1;
487
+ min-height: 0;
488
+ }
489
+
490
+ /* Mobile - 固定尺寸,禁用拖拽 */
491
+ @media (max-width: 480px) {
492
+ .panel.dock-right {
493
+ width: 100vw !important;
494
+ right: -100vw;
495
+ }
496
+ .panel.dock-right.open {
497
+ right: 0;
498
+ }
499
+ .panel.dock-bottom {
500
+ height: 50vh !important;
501
+ max-height: 50vh !important;
502
+ bottom: -50vh;
503
+ }
504
+ .panel.dock-bottom.open {
505
+ bottom: 0;
506
+ }
507
+ }
508
+ </style>
509
+
510
+ <hs-floating-trigger></hs-floating-trigger>
511
+
512
+ <div class="panel dock-right">
513
+ <hs-resizer></hs-resizer>
514
+ <hs-task-header></hs-task-header>
515
+ <div class="content">
516
+ <hs-task-list></hs-task-list>
517
+ <hs-task-detail style="display:none"></hs-task-detail>
518
+ <hs-timeline style="display:none"></hs-timeline>
519
+ </div>
520
+ </div>
521
+ `;
522
+ }
523
+ }
524
+
525
+ customElements.define('hs-devtools', DevTools);
@@ -0,0 +1,102 @@
1
+ import { themeStyles } from '../styles/theme.css';
2
+ import { ICONS } from './icons';
3
+
4
+ export class FloatingTrigger extends HTMLElement {
5
+ private _shadow: ShadowRoot;
6
+
7
+ // 配置属性
8
+ private _position: string = 'bottom-right';
9
+ private _bgColor: string = '';
10
+ private _textColor: string = '';
11
+
12
+ static get observedAttributes() {
13
+ return ['position', 'bg-color', 'text-color'];
14
+ }
15
+
16
+ constructor() {
17
+ super();
18
+ this._shadow = this.attachShadow({ mode: 'open' });
19
+ }
20
+
21
+ connectedCallback() {
22
+ this.render();
23
+ this.addEventListeners();
24
+ }
25
+
26
+ attributeChangedCallback(name: string, _oldVal: string, newVal: string) {
27
+ console.log('[FloatingTrigger] attributeChangedCallback:', name, newVal);
28
+ if (name === 'position') {
29
+ this._position = newVal || 'bottom-right';
30
+ } else if (name === 'bg-color') {
31
+ this._bgColor = newVal || '';
32
+ } else if (name === 'text-color') {
33
+ this._textColor = newVal || '';
34
+ }
35
+ // 如果已经渲染,重新渲染以应用新样式
36
+ if (this._shadow.querySelector('button')) {
37
+ console.log('[FloatingTrigger] re-rendering with:', this._position, this._bgColor, this._textColor);
38
+ this.render();
39
+ this.addEventListeners();
40
+ }
41
+ }
42
+
43
+ addEventListeners() {
44
+ const btn = this._shadow.querySelector('button');
45
+ btn?.addEventListener('click', () => {
46
+ this.dispatchEvent(new CustomEvent('toggle', { bubbles: true, composed: true }));
47
+ });
48
+ }
49
+
50
+ render() {
51
+ // 计算位置样式 - 必须明确设置所有四个方向,避免默认值干扰
52
+ const pos = this._position;
53
+ const posStyles = `
54
+ top: ${pos.includes('top') ? '20px' : 'auto'};
55
+ bottom: ${pos.includes('bottom') ? '20px' : 'auto'};
56
+ left: ${pos.includes('left') ? '20px' : 'auto'};
57
+ right: ${pos.includes('right') ? '20px' : 'auto'};
58
+ `;
59
+
60
+ // 自定义颜色
61
+ const bgStyle = this._bgColor ? `background: ${this._bgColor};` : '';
62
+ const colorStyle = this._textColor ? `color: ${this._textColor};` : '';
63
+
64
+ this._shadow.innerHTML = `
65
+ <style>
66
+ ${themeStyles}
67
+ button {
68
+ position: fixed;
69
+ ${posStyles}
70
+ width: 48px;
71
+ height: 48px;
72
+ border-radius: 50%;
73
+ background: var(--hs-primary);
74
+ color: white;
75
+ ${bgStyle}
76
+ ${colorStyle}
77
+ border: none;
78
+ box-shadow: var(--hs-shadow);
79
+ cursor: pointer;
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ font-family: var(--hs-font-family);
84
+ z-index: var(--hs-z-index);
85
+ transition: transform 0.2s;
86
+ }
87
+ button:hover {
88
+ ${this._bgColor ? `background: ${this._bgColor}; filter: brightness(1.1);` : 'background: var(--hs-primary-hover);'}
89
+ transform: scale(1.1);
90
+ }
91
+ button:active {
92
+ transform: scale(0.95);
93
+ }
94
+ </style>
95
+ <button title="Toggle Hyper Scheduler DevTools">
96
+ ${ICONS.chart}
97
+ </button>
98
+ `;
99
+ }
100
+ }
101
+
102
+ customElements.define('hs-floating-trigger', FloatingTrigger);
@@ -0,0 +1,16 @@
1
+ export const ICONS = {
2
+ back: `<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="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>`,
3
+ trigger: `<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"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`,
4
+ pause: `<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="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>`,
5
+ resume: `<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"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>`,
6
+ remove: `<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"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`,
7
+ close: `<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="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
8
+ settings: `<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"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`,
9
+ sun: `<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"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`,
10
+ moon: `<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"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>`,
11
+ dock: `<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="2" y1="15" x2="22" y2="15"></line></svg>`,
12
+ 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>`,
13
+ dockBottom: `<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="2" y1="15" x2="22" y2="15"></line></svg>`,
14
+ 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>`,
15
+ list: `<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="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`
16
+ };