bosun 0.29.0 → 0.29.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/package.json +5 -2
- package/ui/app.js +141 -111
- package/ui/components/command-palette.js +139 -17
- package/ui/components/diff-viewer.js +32 -32
- package/ui/components/forms.js +98 -216
- package/ui/components/session-list.js +41 -40
- package/ui/components/shared.js +86 -86
- package/ui/components/workspace-switcher.js +66 -69
- package/ui/index.html +0 -91
- package/ui/styles/components.css +3413 -322
- package/ui/styles/layout.css +488 -5
- package/ui/styles.css +225 -0
- package/ui/tabs/agents.js +504 -533
- package/ui/tabs/chat.js +16 -16
- package/ui/tabs/control.js +74 -76
- package/ui/tabs/dashboard.js +107 -113
- package/ui/tabs/infra.js +67 -76
- package/ui/tabs/logs.js +62 -71
- package/ui/tabs/settings.js +69 -26
- package/ui/tabs/tasks.js +53 -56
- package/ui/tabs/telemetry.js +30 -30
- package/desktop/AGENTS.md +0 -62
- package/desktop/package-lock.json +0 -4193
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -127,7 +127,10 @@
|
|
|
127
127
|
"conflict-resolver.mjs",
|
|
128
128
|
"copilot-shell.mjs",
|
|
129
129
|
"diff-stats.mjs",
|
|
130
|
-
"desktop/",
|
|
130
|
+
"desktop/main.mjs",
|
|
131
|
+
"desktop/launch.mjs",
|
|
132
|
+
"desktop/preload.mjs",
|
|
133
|
+
"desktop/package.json",
|
|
131
134
|
"desktop-shortcut.mjs",
|
|
132
135
|
"error-detector.mjs",
|
|
133
136
|
"fetch-runtime.mjs",
|
package/ui/app.js
CHANGED
|
@@ -180,15 +180,15 @@ function useBackendHealth() {
|
|
|
180
180
|
function OfflineBanner() {
|
|
181
181
|
const { retry: manualRetry } = useBackendHealth();
|
|
182
182
|
return html`
|
|
183
|
-
<div class="offline-banner
|
|
184
|
-
<
|
|
185
|
-
<div class="
|
|
186
|
-
<div class="
|
|
187
|
-
<div class="
|
|
183
|
+
<div class="offline-banner">
|
|
184
|
+
<div class="offline-banner-icon">⚠️</div>
|
|
185
|
+
<div class="offline-banner-content">
|
|
186
|
+
<div class="offline-banner-title">Backend Unreachable</div>
|
|
187
|
+
<div class="offline-banner-meta">${backendError.value || "Connection lost"}</div>
|
|
188
188
|
${backendLastSeen.value
|
|
189
|
-
? html`<div class="
|
|
189
|
+
? html`<div class="offline-banner-meta">Last connected: ${formatTimeAgo(backendLastSeen.value)}</div>`
|
|
190
190
|
: null}
|
|
191
|
-
<div class="
|
|
191
|
+
<div class="offline-banner-meta">Retry attempt #${backendRetryCount.value}</div>
|
|
192
192
|
</div>
|
|
193
193
|
<button class="btn btn-ghost btn-sm" onClick=${manualRetry}>Retry</button>
|
|
194
194
|
</div>
|
|
@@ -253,19 +253,27 @@ function Header() {
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
return html`
|
|
256
|
-
<header class="app-header
|
|
257
|
-
<div class="
|
|
258
|
-
|
|
256
|
+
<header class="app-header">
|
|
257
|
+
<div class="app-header-left">
|
|
258
|
+
<div class="app-header-workspace">
|
|
259
|
+
<${WorkspaceSwitcher} />
|
|
260
|
+
</div>
|
|
259
261
|
</div>
|
|
260
|
-
<div class="
|
|
261
|
-
<div class="
|
|
262
|
-
<div class="
|
|
263
|
-
<
|
|
264
|
-
|
|
262
|
+
<div class="app-header-right">
|
|
263
|
+
<div class="header-actions">
|
|
264
|
+
<div class="header-status">
|
|
265
|
+
<div class="connection-pill ${connClass}">
|
|
266
|
+
<span class="connection-dot"></span>
|
|
267
|
+
${connLabel}
|
|
268
|
+
</div>
|
|
269
|
+
${freshnessLabel
|
|
270
|
+
? html`<div class="header-freshness">${freshnessLabel}</div>`
|
|
271
|
+
: null}
|
|
265
272
|
</div>
|
|
266
|
-
${
|
|
273
|
+
${user
|
|
274
|
+
? html`<div class="app-header-user">@${user.username || user.first_name}</div>`
|
|
275
|
+
: null}
|
|
267
276
|
</div>
|
|
268
|
-
${user ? html`<div class="text-xs opacity-60">@${user.username || user.first_name}</div>` : null}
|
|
269
277
|
</div>
|
|
270
278
|
</header>
|
|
271
279
|
`;
|
|
@@ -278,49 +286,51 @@ function SidebarNav() {
|
|
|
278
286
|
const user = getTelegramUser();
|
|
279
287
|
const isConn = connected.value;
|
|
280
288
|
return html`
|
|
281
|
-
<aside class="sidebar
|
|
282
|
-
<div class="
|
|
283
|
-
<
|
|
284
|
-
|
|
289
|
+
<aside class="sidebar">
|
|
290
|
+
<div class="sidebar-brand">
|
|
291
|
+
<div class="sidebar-logo">
|
|
292
|
+
<img src="logo.png" alt="Bosun" class="app-logo-img" />
|
|
293
|
+
</div>
|
|
294
|
+
<div class="sidebar-title">Bosun</div>
|
|
285
295
|
</div>
|
|
286
|
-
<div class="
|
|
287
|
-
<button class="btn btn-primary btn-
|
|
288
|
-
|
|
296
|
+
<div class="sidebar-actions">
|
|
297
|
+
<button class="btn btn-primary btn-block" onClick=${() => createSession({ type: "primary" })}>
|
|
298
|
+
<span class="btn-icon">✨</span> New Session
|
|
289
299
|
</button>
|
|
290
|
-
<button class="btn btn-ghost btn-
|
|
291
|
-
|
|
300
|
+
<button class="btn btn-ghost btn-block" onClick=${() => navigateTo("tasks")}>
|
|
301
|
+
<span class="btn-icon">📋</span> View Tasks
|
|
292
302
|
</button>
|
|
293
303
|
</div>
|
|
294
|
-
<
|
|
304
|
+
<nav class="sidebar-nav">
|
|
295
305
|
${TAB_CONFIG.map((tab) => {
|
|
296
306
|
const isActive = activeTab.value === tab.id;
|
|
297
307
|
const isHome = tab.id === "dashboard";
|
|
298
308
|
return html`
|
|
299
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
309
|
+
<button
|
|
310
|
+
key=${tab.id}
|
|
311
|
+
class="sidebar-nav-item ${isActive ? "active" : ""}"
|
|
312
|
+
aria-label=${tab.label}
|
|
313
|
+
aria-current=${isActive ? "page" : null}
|
|
314
|
+
onClick=${() =>
|
|
315
|
+
navigateTo(tab.id, {
|
|
305
316
|
resetHistory: isHome,
|
|
306
317
|
forceRefresh: isHome && isActive,
|
|
307
318
|
})}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
</li>
|
|
319
|
+
>
|
|
320
|
+
${ICONS[tab.icon]}
|
|
321
|
+
<span>${tab.label}</span>
|
|
322
|
+
</button>
|
|
313
323
|
`;
|
|
314
324
|
})}
|
|
315
|
-
</
|
|
316
|
-
<div class="
|
|
317
|
-
<div class="
|
|
318
|
-
<span class="
|
|
319
|
-
|
|
325
|
+
</nav>
|
|
326
|
+
<div class="sidebar-footer">
|
|
327
|
+
<div class="sidebar-status ${isConn ? "online" : "offline"}">
|
|
328
|
+
<span class="sidebar-status-dot"></span>
|
|
329
|
+
${isConn ? "Connected" : "Offline"}
|
|
320
330
|
</div>
|
|
321
331
|
${user
|
|
322
|
-
? html`<div class="
|
|
323
|
-
: html`<div class="
|
|
332
|
+
? html`<div class="sidebar-user">@${user.username || user.first_name || "operator"}</div>`
|
|
333
|
+
: html`<div class="sidebar-user">Operator Console</div>`}
|
|
324
334
|
</div>
|
|
325
335
|
</aside>
|
|
326
336
|
`;
|
|
@@ -354,21 +364,29 @@ function SessionRail({ onResizeStart, onResizeReset, showResizer }) {
|
|
|
354
364
|
}, [sessionsData.value, selectedSessionId.value]);
|
|
355
365
|
|
|
356
366
|
return html`
|
|
357
|
-
<aside class="session-rail
|
|
358
|
-
<div class="
|
|
359
|
-
<div class="
|
|
360
|
-
<div class="
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
<${SessionList} showArchived=${showArchived} onToggleArchived=${setShowArchived} defaultType="primary" />
|
|
367
|
+
<aside class="session-rail">
|
|
368
|
+
<div class="rail-header">
|
|
369
|
+
<div class="rail-title">Sessions</div>
|
|
370
|
+
<div class="rail-meta">
|
|
371
|
+
${activeCount} active · ${sessions.length} total
|
|
372
|
+
</div>
|
|
364
373
|
</div>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
374
|
+
<${SessionList}
|
|
375
|
+
showArchived=${showArchived}
|
|
376
|
+
onToggleArchived=${setShowArchived}
|
|
377
|
+
defaultType="primary"
|
|
378
|
+
/>
|
|
379
|
+
${showResizer
|
|
380
|
+
? html`
|
|
381
|
+
<div
|
|
382
|
+
class="rail-resizer"
|
|
383
|
+
role="separator"
|
|
384
|
+
aria-label="Resize sessions panel"
|
|
385
|
+
onPointerDown=${(e) => onResizeStart("rail", e)}
|
|
386
|
+
onDoubleClick=${() => onResizeReset("rail")}
|
|
387
|
+
></div>
|
|
388
|
+
`
|
|
389
|
+
: null}
|
|
372
390
|
</aside>
|
|
373
391
|
`;
|
|
374
392
|
}
|
|
@@ -448,34 +466,32 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
|
|
|
448
466
|
}, [isSessionTab, sessionId, session?.taskId, session?.branch, status]);
|
|
449
467
|
|
|
450
468
|
return html`
|
|
451
|
-
<aside class="inspector
|
|
452
|
-
<div class="inspector-section
|
|
453
|
-
<div class="inspector-title
|
|
469
|
+
<aside class="inspector">
|
|
470
|
+
<div class="inspector-section">
|
|
471
|
+
<div class="inspector-title">Focus</div>
|
|
454
472
|
${session
|
|
455
473
|
? html`
|
|
456
|
-
<div class="
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
<div class="inspector-kv flex justify-between text-xs"><span class="opacity-50">Preview</span><strong>${preview}</strong></div>
|
|
462
|
-
</div>
|
|
474
|
+
<div class="inspector-kv"><span>Session</span><strong>${session.title || session.taskId || session.id}</strong></div>
|
|
475
|
+
<div class="inspector-kv"><span>Status</span><strong>${status}</strong></div>
|
|
476
|
+
<div class="inspector-kv"><span>Type</span><strong>${type}</strong></div>
|
|
477
|
+
<div class="inspector-kv"><span>Last Active</span><strong>${lastActive ? formatRelative(lastActive) : "—"}</strong></div>
|
|
478
|
+
<div class="inspector-kv"><span>Preview</span><strong>${preview}</strong></div>
|
|
463
479
|
`
|
|
464
|
-
: html`<div class="inspector-empty
|
|
480
|
+
: html`<div class="inspector-empty">Select a session to see context.</div>`}
|
|
465
481
|
</div>
|
|
466
482
|
|
|
467
483
|
${isSessionTab
|
|
468
484
|
? html`
|
|
469
|
-
<div class="inspector-section inspector-scroll
|
|
470
|
-
<div class="inspector-title
|
|
485
|
+
<div class="inspector-section inspector-scroll">
|
|
486
|
+
<div class="inspector-title">Latest Diff</div>
|
|
471
487
|
<${DiffViewer} sessionId=${sessionId} />
|
|
472
488
|
</div>
|
|
473
|
-
<div class="inspector-section
|
|
474
|
-
<div class="inspector-title
|
|
489
|
+
<div class="inspector-section">
|
|
490
|
+
<div class="inspector-title">Smart Logs</div>
|
|
475
491
|
${logState === "error"
|
|
476
|
-
? html`<div class="inspector-empty
|
|
492
|
+
? html`<div class="inspector-empty">Log stream unavailable.</div>`
|
|
477
493
|
: smartLogs.length === 0
|
|
478
|
-
? html`<div class="inspector-empty
|
|
494
|
+
? html`<div class="inspector-empty">No noteworthy logs right now.</div>`
|
|
479
495
|
: html`
|
|
480
496
|
<div class="inspector-scroll">
|
|
481
497
|
${smartLogs.map(
|
|
@@ -498,17 +514,17 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
|
|
|
498
514
|
</div>
|
|
499
515
|
`
|
|
500
516
|
: html`
|
|
501
|
-
<div class="inspector-section
|
|
502
|
-
<div class="inspector-title
|
|
503
|
-
<div class="inspector-kv
|
|
504
|
-
<div class="inspector-kv
|
|
505
|
-
<div class="inspector-kv
|
|
517
|
+
<div class="inspector-section">
|
|
518
|
+
<div class="inspector-title">System Pulse</div>
|
|
519
|
+
<div class="inspector-kv"><span>API</span><strong>${connected.value ? "Connected" : "Offline"}</strong></div>
|
|
520
|
+
<div class="inspector-kv"><span>WebSocket</span><strong>${wsConnected.value ? "Live" : "Closed"}</strong></div>
|
|
521
|
+
<div class="inspector-kv"><span>Last Seen</span><strong>${backendLastSeen.value ? formatRelative(backendLastSeen.value) : "—"}</strong></div>
|
|
506
522
|
</div>
|
|
507
523
|
`}
|
|
508
524
|
${showResizer
|
|
509
525
|
? html`
|
|
510
526
|
<div
|
|
511
|
-
class="inspector-resizer
|
|
527
|
+
class="inspector-resizer"
|
|
512
528
|
role="separator"
|
|
513
529
|
aria-label="Resize inspector panel"
|
|
514
530
|
onPointerDown=${(e) => onResizeStart("inspector", e)}
|
|
@@ -535,23 +551,37 @@ function getTabsById(ids) {
|
|
|
535
551
|
function BottomNav({ compact, moreOpen, onToggleMore, onNavigate }) {
|
|
536
552
|
const primaryTabs = getTabsById(PRIMARY_NAV_TABS);
|
|
537
553
|
return html`
|
|
538
|
-
<nav class
|
|
554
|
+
<nav class=${`bottom-nav ${compact ? "compact" : ""}`}>
|
|
539
555
|
${primaryTabs.map((tab) => {
|
|
540
556
|
const isHome = tab.id === "dashboard";
|
|
541
557
|
const isActive = activeTab.value === tab.id;
|
|
542
558
|
return html`
|
|
543
|
-
<button
|
|
544
|
-
|
|
545
|
-
|
|
559
|
+
<button
|
|
560
|
+
key=${tab.id}
|
|
561
|
+
class="nav-item ${isActive ? "active" : ""}"
|
|
562
|
+
aria-label=${`Go to ${tab.label}`}
|
|
563
|
+
type="button"
|
|
564
|
+
onClick=${() =>
|
|
565
|
+
onNavigate(tab.id, {
|
|
566
|
+
resetHistory: isHome,
|
|
567
|
+
forceRefresh: isHome && isActive,
|
|
568
|
+
})}
|
|
569
|
+
>
|
|
546
570
|
${ICONS[tab.icon]}
|
|
547
|
-
<span class="
|
|
571
|
+
<span class="nav-label">${tab.label}</span>
|
|
548
572
|
</button>
|
|
549
573
|
`;
|
|
550
574
|
})}
|
|
551
|
-
<button
|
|
552
|
-
|
|
575
|
+
<button
|
|
576
|
+
class="nav-item nav-item-more ${moreOpen ? "active" : ""}"
|
|
577
|
+
aria-haspopup="dialog"
|
|
578
|
+
aria-expanded=${moreOpen ? "true" : "false"}
|
|
579
|
+
aria-label=${moreOpen ? "Close more menu" : "Open more menu"}
|
|
580
|
+
type="button"
|
|
581
|
+
onClick=${onToggleMore}
|
|
582
|
+
>
|
|
553
583
|
${ICONS.ellipsis}
|
|
554
|
-
<span class="
|
|
584
|
+
<span class="nav-label">More</span>
|
|
555
585
|
</button>
|
|
556
586
|
</nav>
|
|
557
587
|
`;
|
|
@@ -563,47 +593,47 @@ function MoreSheet({ open, onClose, onNavigate }) {
|
|
|
563
593
|
return html`
|
|
564
594
|
<${Modal} title="More" open=${open} onClose=${onClose}>
|
|
565
595
|
<div class="more-menu" role="navigation" aria-label="More menu">
|
|
566
|
-
<div class="more-menu-section
|
|
567
|
-
<div class="more-menu-section-title
|
|
568
|
-
<div class="
|
|
596
|
+
<div class="more-menu-section">
|
|
597
|
+
<div class="more-menu-section-title">Quick Access</div>
|
|
598
|
+
<div class="more-menu-grid">
|
|
569
599
|
${primaryTabs.map((tab) => {
|
|
570
600
|
const isHome = tab.id === "dashboard";
|
|
571
601
|
const isActive = activeTab.value === tab.id;
|
|
572
602
|
return html`
|
|
573
603
|
<button
|
|
574
604
|
key=${tab.id}
|
|
575
|
-
class="
|
|
605
|
+
class="more-menu-item ${isActive ? "active" : ""}"
|
|
576
606
|
aria-label=${`Open ${tab.label}`}
|
|
577
607
|
onClick=${() =>
|
|
578
608
|
onNavigate(tab.id, {
|
|
579
609
|
resetHistory: isHome,
|
|
580
610
|
})}
|
|
581
611
|
>
|
|
582
|
-
<span class="
|
|
583
|
-
<span class="
|
|
612
|
+
<span class="more-menu-icon">${ICONS[tab.icon]}</span>
|
|
613
|
+
<span class="more-menu-label">${tab.label}</span>
|
|
584
614
|
</button>
|
|
585
615
|
`;
|
|
586
616
|
})}
|
|
587
617
|
</div>
|
|
588
618
|
</div>
|
|
589
619
|
<div class="more-menu-section">
|
|
590
|
-
<div class="more-menu-section-title
|
|
591
|
-
<div class="
|
|
620
|
+
<div class="more-menu-section-title">Explore</div>
|
|
621
|
+
<div class="more-menu-grid">
|
|
592
622
|
${moreTabs.map((tab) => {
|
|
593
623
|
const isHome = tab.id === "dashboard";
|
|
594
624
|
const isActive = activeTab.value === tab.id;
|
|
595
625
|
return html`
|
|
596
626
|
<button
|
|
597
627
|
key=${tab.id}
|
|
598
|
-
class="
|
|
628
|
+
class="more-menu-item ${isActive ? "active" : ""}"
|
|
599
629
|
aria-label=${`Open ${tab.label}`}
|
|
600
630
|
onClick=${() =>
|
|
601
631
|
onNavigate(tab.id, {
|
|
602
632
|
resetHistory: isHome,
|
|
603
633
|
})}
|
|
604
634
|
>
|
|
605
|
-
<span class="
|
|
606
|
-
<span class="
|
|
635
|
+
<span class="more-menu-icon">${ICONS[tab.icon]}</span>
|
|
636
|
+
<span class="more-menu-label">${tab.label}</span>
|
|
607
637
|
</button>
|
|
608
638
|
`;
|
|
609
639
|
})}
|
|
@@ -967,7 +997,7 @@ function App() {
|
|
|
967
997
|
|
|
968
998
|
return html`
|
|
969
999
|
<div
|
|
970
|
-
class="app-shell
|
|
1000
|
+
class="app-shell"
|
|
971
1001
|
style=${shellStyle}
|
|
972
1002
|
data-tab=${activeTab.value}
|
|
973
1003
|
data-has-rail=${showSessionRail ? "true" : "false"}
|
|
@@ -978,8 +1008,8 @@ function App() {
|
|
|
978
1008
|
${/* Sidebar drawer overlay for tablet */ ""}
|
|
979
1009
|
${sidebarDrawerOpen && !isDesktop
|
|
980
1010
|
? html`
|
|
981
|
-
<div class="drawer-overlay
|
|
982
|
-
<div class="drawer drawer-left
|
|
1011
|
+
<div class="drawer-overlay" onClick=${closeDrawers}></div>
|
|
1012
|
+
<div class="drawer drawer-left">
|
|
983
1013
|
<${SidebarNav} />
|
|
984
1014
|
</div>
|
|
985
1015
|
`
|
|
@@ -988,8 +1018,8 @@ function App() {
|
|
|
988
1018
|
${/* Inspector drawer overlay for tablet */ ""}
|
|
989
1019
|
${inspectorDrawerOpen && !isDesktop
|
|
990
1020
|
? html`
|
|
991
|
-
<div class="drawer-overlay
|
|
992
|
-
<div class="drawer drawer-right
|
|
1021
|
+
<div class="drawer-overlay" onClick=${closeDrawers}></div>
|
|
1022
|
+
<div class="drawer drawer-right">
|
|
993
1023
|
<${InspectorPanel}
|
|
994
1024
|
onResizeStart=${handleResizeStart}
|
|
995
1025
|
onResizeReset=${handleResizeReset}
|
|
@@ -1006,14 +1036,14 @@ function App() {
|
|
|
1006
1036
|
showResizer=${isDesktop}
|
|
1007
1037
|
/>`
|
|
1008
1038
|
: null}
|
|
1009
|
-
<div class="app-main
|
|
1010
|
-
<div class="main-panel
|
|
1039
|
+
<div class="app-main">
|
|
1040
|
+
<div class="main-panel">
|
|
1011
1041
|
<${Header} />
|
|
1012
1042
|
|
|
1013
1043
|
${/* Tablet action bar with drawer toggles */ ""}
|
|
1014
1044
|
${showDrawerToggles
|
|
1015
1045
|
? html`
|
|
1016
|
-
<div class="tablet-action-bar
|
|
1046
|
+
<div class="tablet-action-bar">
|
|
1017
1047
|
<button
|
|
1018
1048
|
class="btn btn-ghost btn-sm tablet-toggle"
|
|
1019
1049
|
onClick=${toggleSidebar}
|
|
@@ -1040,14 +1070,14 @@ function App() {
|
|
|
1040
1070
|
<${ToastContainer} />
|
|
1041
1071
|
<${CommandPalette} open=${paletteOpen} onClose=${paletteClose} />
|
|
1042
1072
|
<${PullToRefresh} onRefresh=${() => refreshTab(activeTab.value)}>
|
|
1043
|
-
<main class="main-content
|
|
1073
|
+
<main class="main-content" ref=${mainRef}>
|
|
1044
1074
|
<${CurrentTab} />
|
|
1045
1075
|
</main>
|
|
1046
1076
|
<//>
|
|
1047
1077
|
${showScrollTop &&
|
|
1048
1078
|
html`
|
|
1049
1079
|
<button
|
|
1050
|
-
class="scroll-top
|
|
1080
|
+
class="scroll-top"
|
|
1051
1081
|
title="Back to top"
|
|
1052
1082
|
onClick=${() => {
|
|
1053
1083
|
mainRef.current?.scrollTo({ top: 0, behavior: "smooth" });
|