pinokiod 7.3.10 → 7.3.12

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 (51) hide show
  1. package/kernel/bin/brew.js +12 -2
  2. package/kernel/bin/caddy.js +24 -20
  3. package/kernel/bin/huggingface.js +2 -2
  4. package/kernel/bin/setup.js +2 -3
  5. package/kernel/bin/uv.js +13 -6
  6. package/kernel/connect/index.js +5 -1
  7. package/kernel/connect/providers/huggingface/index.js +213 -75
  8. package/kernel/environment.js +16 -1
  9. package/kernel/router/localhost_home_router.js +7 -0
  10. package/kernel/shell.js +1 -5
  11. package/kernel/util.js +1 -0
  12. package/package.json +1 -1
  13. package/server/index.js +75 -33
  14. package/server/public/common.js +45 -71
  15. package/server/public/install.js +20 -2
  16. package/server/public/layout.js +1 -1
  17. package/server/public/style.css +1432 -511
  18. package/server/public/tab-link-popover.css +162 -18
  19. package/server/public/tab-link-popover.js +230 -21
  20. package/server/public/task-launcher.css +121 -84
  21. package/server/public/terminal-settings.js +121 -45
  22. package/server/public/universal-launcher.css +42 -33
  23. package/server/public/urldropdown.css +284 -0
  24. package/server/views/app.ejs +1608 -321
  25. package/server/views/autolaunch.ejs +4 -5
  26. package/server/views/checkpoints.ejs +223 -50
  27. package/server/views/connect/huggingface.ejs +406 -325
  28. package/server/views/connect.ejs +0 -1
  29. package/server/views/github.ejs +277 -324
  30. package/server/views/index.ejs +53 -1
  31. package/server/views/install.ejs +119 -19
  32. package/server/views/logs.ejs +10 -9
  33. package/server/views/net.ejs +341 -64
  34. package/server/views/network.ejs +85 -63
  35. package/server/views/partials/main_sidebar.ejs +237 -12
  36. package/server/views/plugins.ejs +141 -3
  37. package/server/views/settings.ejs +103 -7
  38. package/server/views/setup.ejs +0 -5
  39. package/server/views/skills.ejs +0 -1
  40. package/server/views/task_list.ejs +0 -1
  41. package/server/views/terminal.ejs +142 -1
  42. package/server/views/terminals.ejs +346 -6
  43. package/server/views/tools.ejs +828 -1691
  44. package/test/caddy-install.test.js +53 -0
  45. package/test/connect-setup.test.js +16 -0
  46. package/test/github-connection.test.js +1 -1
  47. package/test/huggingface-bin.test.js +4 -4
  48. package/test/huggingface-connect.test.js +73 -0
  49. package/test/main-sidebar.test.js +31 -0
  50. package/test/shell-run-template.test.js +5 -1
  51. package/test/uv-bin.test.js +29 -0
@@ -1,42 +1,124 @@
1
1
  .tab-link-popover {
2
2
  position: fixed;
3
3
  display: none;
4
- flex-direction: column;
5
- gap: 4px;
6
- padding: 8px 0;
7
- border-radius: 10px;
8
- border: 1px solid rgba(0, 0, 0, 0.12);
9
- background: rgba(255, 255, 255, 0.97);
10
- box-shadow: 0 12px 32px -8px rgba(15, 23, 42, 0.25);
11
4
  z-index: 9999;
5
+ box-sizing: border-box;
12
6
  min-width: 240px;
13
7
  max-width: 420px;
14
- backdrop-filter: blur(6px);
15
- }
16
- body.dark .tab-link-popover {
17
- border-color: rgba(255, 255, 255, 0.08);
18
- background: rgba(17, 24, 39, 0.95);
19
- box-shadow: 0 12px 36px -12px rgba(15, 23, 42, 0.55);
8
+ color: var(--pinokio-sidebar-tab-active-color, rgba(15, 23, 42, 0.9));
20
9
  }
21
10
  .tab-link-popover.visible {
11
+ display: block;
12
+ }
13
+ .tab-link-popover--modal {
14
+ inset: 0;
15
+ z-index: 10000060;
16
+ min-width: 0;
17
+ max-width: none;
18
+ padding: 16px;
19
+ }
20
+ .tab-link-popover--modal.visible {
21
+ display: grid;
22
+ place-items: center;
23
+ }
24
+ body.tab-link-popover-open {
25
+ overflow: hidden;
26
+ }
27
+ .tab-link-popover .tab-link-popover-backdrop {
28
+ display: none;
29
+ }
30
+ .tab-link-popover--modal .tab-link-popover-backdrop {
31
+ position: absolute;
32
+ inset: 0;
33
+ display: block;
34
+ width: 100%;
35
+ height: 100%;
36
+ appearance: none;
37
+ border: 0;
38
+ border-radius: 0;
39
+ padding: 0;
40
+ background: rgba(15, 23, 42, 0.28);
41
+ cursor: default;
42
+ }
43
+ body.dark .tab-link-popover--modal .tab-link-popover-backdrop {
44
+ background: rgba(0, 0, 0, 0.44);
45
+ }
46
+ .tab-link-popover .tab-link-popover-surface {
47
+ position: relative;
48
+ width: 100%;
49
+ min-width: inherit;
50
+ max-width: inherit;
51
+ max-height: calc(100vh - 24px);
22
52
  display: flex;
53
+ flex-direction: column;
54
+ gap: 4px;
55
+ box-sizing: border-box;
56
+ overflow-y: auto;
57
+ padding: 8px 0;
58
+ border-radius: 8px;
59
+ border: 1px solid var(--pinokio-sidebar-tabbar-border, rgba(15, 23, 42, 0.08));
60
+ background: var(--pinokio-sidebar-tabbar-bg, rgba(255, 255, 255, 0.98));
61
+ color: inherit;
62
+ box-shadow: 0 18px 45px rgba(15, 23, 42, 0.18);
63
+ }
64
+ body.dark .tab-link-popover .tab-link-popover-surface {
65
+ box-shadow: 0 18px 45px rgba(0, 0, 0, 0.42);
66
+ }
67
+ .tab-link-popover--modal .tab-link-popover-surface {
68
+ width: min(460px, calc(100vw - 32px));
69
+ min-width: 0;
70
+ max-width: 460px;
71
+ max-height: calc(100dvh - 32px);
72
+ padding-bottom: 10px;
23
73
  }
24
74
  .tab-link-popover .tab-link-popover-header {
75
+ display: grid;
76
+ grid-template-columns: minmax(0, 1fr) 28px;
77
+ align-items: center;
78
+ gap: 8px;
79
+ padding: 10px 10px 6px 14px;
80
+ }
81
+ .tab-link-popover--anchored .tab-link-popover-header {
82
+ grid-template-columns: minmax(0, 1fr);
83
+ padding-right: 14px;
84
+ }
85
+ .tab-link-popover .tab-link-popover-title {
86
+ min-width: 0;
25
87
  display: flex;
26
88
  align-items: center;
27
89
  gap: 8px;
28
- padding: 10px 14px 6px;
29
90
  font-size: 11px;
30
91
  font-weight: 600;
31
92
  letter-spacing: 0.05em;
32
93
  text-transform: uppercase;
33
- color: rgba(15, 23, 42, 0.55);
94
+ color: var(--pinokio-sidebar-action-muted, rgba(15, 23, 42, 0.55));
34
95
  }
35
- .tab-link-popover .tab-link-popover-header i {
96
+ .tab-link-popover .tab-link-popover-title-icon i {
36
97
  font-size: 12px;
37
98
  }
38
- body.dark .tab-link-popover .tab-link-popover-header {
39
- color: rgba(226, 232, 240, 0.7);
99
+ .tab-link-popover .tab-link-popover-close {
100
+ width: 28px;
101
+ height: 28px;
102
+ min-width: 28px;
103
+ display: inline-flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ appearance: none;
107
+ border: 0;
108
+ border-radius: 6px;
109
+ background: transparent;
110
+ color: var(--pinokio-sidebar-icon-muted, rgba(31, 41, 55, 0.58));
111
+ cursor: pointer;
112
+ transition: background-color 120ms ease, color 120ms ease;
113
+ }
114
+ .tab-link-popover--anchored .tab-link-popover-close {
115
+ display: none;
116
+ }
117
+ .tab-link-popover .tab-link-popover-close:hover,
118
+ .tab-link-popover .tab-link-popover-close:focus-visible {
119
+ background: var(--pinokio-sidebar-tab-hover, rgba(15, 23, 42, 0.06));
120
+ color: var(--pinokio-sidebar-icon-active, rgba(17, 24, 39, 0.84));
121
+ outline: none;
40
122
  }
41
123
  .tab-link-popover .tab-link-popover-item {
42
124
  width: 100%;
@@ -115,6 +197,20 @@ body.dark .tab-link-popover .tab-link-popover-item--action .value {
115
197
  flex: 0 0 auto;
116
198
  margin-left: auto;
117
199
  }
200
+ .tab-link-popover--modal .tab-link-popover-item.qr-inline {
201
+ align-items: center;
202
+ gap: 16px;
203
+ padding-top: 12px;
204
+ padding-bottom: 12px;
205
+ }
206
+ .tab-link-popover--modal .tab-link-popover-item.qr-inline .qr {
207
+ width: 148px;
208
+ height: 148px;
209
+ padding: 8px;
210
+ border-radius: 6px;
211
+ background: #ffffff;
212
+ box-sizing: border-box;
213
+ }
118
214
  .tab-link-popover .tab-link-popover-item:hover,
119
215
  .tab-link-popover .tab-link-popover-item:focus-visible {
120
216
  background: rgba(15, 23, 42, 0.06);
@@ -201,6 +297,54 @@ body.dark .tab-link-popover .tab-link-popover-footer:hover,
201
297
  body.dark .tab-link-popover .tab-link-popover-footer:focus-visible {
202
298
  background: rgba(147, 197, 253, 0.35);
203
299
  }
300
+ @media only screen and (max-width: 600px) {
301
+ .tab-link-popover--modal {
302
+ padding: 0;
303
+ }
304
+ .tab-link-popover--modal .tab-link-popover-backdrop {
305
+ background: transparent;
306
+ }
307
+ .tab-link-popover--modal .tab-link-popover-surface {
308
+ width: 100vw;
309
+ min-width: 0;
310
+ max-width: none;
311
+ min-height: 100dvh;
312
+ max-height: 100dvh;
313
+ border-radius: 0;
314
+ border-left: 0;
315
+ border-right: 0;
316
+ box-shadow: none;
317
+ }
318
+ .tab-link-popover--modal .tab-link-popover-header {
319
+ position: sticky;
320
+ top: 0;
321
+ z-index: 1;
322
+ padding: 12px 12px 8px 16px;
323
+ background: var(--pinokio-sidebar-tabbar-bg, rgba(255, 255, 255, 0.98));
324
+ }
325
+ .tab-link-popover--modal .tab-link-popover-item {
326
+ padding-right: 16px;
327
+ padding-left: 16px;
328
+ }
329
+ .tab-link-popover--modal .tab-link-popover-item--action {
330
+ min-height: 44px;
331
+ }
332
+ .tab-link-popover--modal .tab-link-popover-item.qr-inline {
333
+ display: grid;
334
+ grid-template-columns: minmax(0, 1fr);
335
+ justify-items: start;
336
+ gap: 10px;
337
+ }
338
+ .tab-link-popover--modal .tab-link-popover-item.qr-inline .qr {
339
+ width: min(220px, 64vw);
340
+ height: min(220px, 64vw);
341
+ margin: 6px auto 0;
342
+ }
343
+ .tab-link-popover--modal .tab-link-popover-section {
344
+ padding-right: 16px;
345
+ padding-left: 16px;
346
+ }
347
+ }
204
348
  .appcanvas > aside .menu-container .tab-link-popover-host {
205
349
  display: flex;
206
350
  align-items: center;
@@ -11,8 +11,12 @@
11
11
  let tabLinkRouterHttpsActive = null
12
12
  let tabLinkPeerInfoPromise = null
13
13
  let tabLinkPeerInfoExpiry = 0
14
+ let tabLinkPopoverMode = "anchored"
15
+ let tabLinkReturnFocusEl = null
14
16
  const TAB_LINK_TRIGGER_CLASS = "tab-link-popover-trigger"
15
17
  const TAB_LINK_TRIGGER_HOST_CLASS = "tab-link-popover-host"
18
+ const TAB_LINK_POPOVER_SURFACE_CLASS = "tab-link-popover-surface"
19
+ const TAB_LINK_POPOVER_TITLE_ID = `${TAB_LINK_POPOVER_ID}-title`
16
20
 
17
21
  const shouldAttachTabLinkTrigger = (link) => {
18
22
  if (!link || !link.classList || !link.classList.contains("frame-link")) {
@@ -126,11 +130,105 @@
126
130
  return { available: false, enabled: true }
127
131
  }
128
132
 
133
+ const getTabLinkPopoverSurface = (popover) => {
134
+ if (!popover || typeof popover.querySelector !== "function") {
135
+ return null
136
+ }
137
+ return popover.querySelector(`.${TAB_LINK_POPOVER_SURFACE_CLASS}`)
138
+ }
139
+
140
+ const getTabLinkFocusables = (root) => {
141
+ if (!root || typeof root.querySelectorAll !== "function") {
142
+ return []
143
+ }
144
+ return Array.from(root.querySelectorAll([
145
+ 'a[href]',
146
+ 'button:not([disabled])',
147
+ 'input:not([disabled])',
148
+ 'select:not([disabled])',
149
+ 'textarea:not([disabled])',
150
+ '[tabindex]:not([tabindex="-1"])'
151
+ ].join(','))).filter((element) => {
152
+ if (!(element instanceof HTMLElement)) {
153
+ return false
154
+ }
155
+ if (element.hidden || element.getAttribute("aria-hidden") === "true") {
156
+ return false
157
+ }
158
+ const rect = element.getBoundingClientRect()
159
+ return rect.width > 0 || rect.height > 0
160
+ })
161
+ }
162
+
163
+ const resetTabLinkPopoverContent = (popover, mode) => {
164
+ if (!popover) {
165
+ return null
166
+ }
167
+ tabLinkPopoverMode = mode === "modal" ? "modal" : "anchored"
168
+ popover.classList.toggle("tab-link-popover--modal", tabLinkPopoverMode === "modal")
169
+ popover.classList.toggle("tab-link-popover--anchored", tabLinkPopoverMode !== "modal")
170
+ popover.setAttribute("data-mode", tabLinkPopoverMode)
171
+ popover.removeAttribute("aria-labelledby")
172
+ popover.removeAttribute("aria-modal")
173
+ popover.removeAttribute("role")
174
+ const surface = getTabLinkPopoverSurface(popover)
175
+ if (surface) {
176
+ surface.innerHTML = ""
177
+ surface.removeAttribute("aria-labelledby")
178
+ surface.removeAttribute("aria-modal")
179
+ surface.removeAttribute("role")
180
+ surface.removeAttribute("tabindex")
181
+ if (tabLinkPopoverMode === "modal") {
182
+ surface.setAttribute("role", "dialog")
183
+ surface.setAttribute("aria-modal", "true")
184
+ surface.setAttribute("aria-labelledby", TAB_LINK_POPOVER_TITLE_ID)
185
+ surface.setAttribute("tabindex", "-1")
186
+ }
187
+ }
188
+ return surface
189
+ }
190
+
191
+ const createTabLinkPopoverHeader = (title = "Tab actions") => {
192
+ const header = document.createElement("div")
193
+ header.className = "tab-link-popover-header"
194
+
195
+ const heading = document.createElement("div")
196
+ heading.className = "tab-link-popover-title"
197
+ heading.id = TAB_LINK_POPOVER_TITLE_ID
198
+
199
+ const icon = document.createElement("span")
200
+ icon.className = "tab-link-popover-title-icon"
201
+ icon.innerHTML = '<i class="fa-solid fa-ellipsis" aria-hidden="true"></i>'
202
+
203
+ const text = document.createElement("span")
204
+ text.textContent = title
205
+ heading.append(icon, text)
206
+
207
+ const close = document.createElement("button")
208
+ close.type = "button"
209
+ close.className = "tab-link-popover-close"
210
+ close.setAttribute("data-tab-link-popover-close", "")
211
+ close.setAttribute("aria-label", "Close tab actions")
212
+ close.innerHTML = '<i class="fa-solid fa-xmark" aria-hidden="true"></i>'
213
+
214
+ header.append(heading, close)
215
+ return header
216
+ }
217
+
129
218
  const ensureTabLinkPopoverEl = () => {
130
219
  if (!tabLinkPopoverEl) {
131
220
  tabLinkPopoverEl = document.createElement("div")
132
221
  tabLinkPopoverEl.id = TAB_LINK_POPOVER_ID
133
- tabLinkPopoverEl.className = "tab-link-popover"
222
+ tabLinkPopoverEl.className = "tab-link-popover tab-link-popover--anchored"
223
+ const backdrop = document.createElement("button")
224
+ backdrop.type = "button"
225
+ backdrop.className = "tab-link-popover-backdrop"
226
+ backdrop.setAttribute("data-tab-link-popover-close", "")
227
+ backdrop.setAttribute("aria-label", "Close tab actions")
228
+ backdrop.tabIndex = -1
229
+ const surface = document.createElement("div")
230
+ surface.className = TAB_LINK_POPOVER_SURFACE_CLASS
231
+ tabLinkPopoverEl.append(backdrop, surface)
134
232
  tabLinkPopoverEl.addEventListener("mouseenter", () => {
135
233
  if (tabLinkHideTimer) {
136
234
  clearTimeout(tabLinkHideTimer)
@@ -143,6 +241,13 @@
143
241
  }
144
242
  })
145
243
  tabLinkPopoverEl.addEventListener("click", (event) => {
244
+ const closeControl = event.target.closest("[data-tab-link-popover-close]")
245
+ if (closeControl && tabLinkPopoverEl.contains(closeControl)) {
246
+ event.preventDefault()
247
+ event.stopPropagation()
248
+ hideTabLinkPopover({ immediate: true })
249
+ return
250
+ }
146
251
  const item = event.target.closest(".tab-link-popover-item")
147
252
  if (!item) {
148
253
  return
@@ -1348,14 +1453,52 @@
1348
1453
  if (!popover || !link) {
1349
1454
  return
1350
1455
  }
1456
+ const surface = getTabLinkPopoverSurface(popover)
1457
+ if (tabLinkPopoverMode === "modal") {
1458
+ popover.style.minWidth = ""
1459
+ popover.style.left = ""
1460
+ popover.style.top = ""
1461
+ popover.style.maxHeight = ""
1462
+ popover.style.overflowY = ""
1463
+ if (surface) {
1464
+ surface.style.maxHeight = ""
1465
+ surface.style.overflowY = ""
1466
+ }
1467
+ popover.style.display = ""
1468
+ popover.style.visibility = ""
1469
+ popover.classList.add("visible")
1470
+ if (document.body) {
1471
+ document.body.classList.add("tab-link-popover-open")
1472
+ }
1473
+ window.requestAnimationFrame(() => {
1474
+ if (!popover.classList.contains("visible")) {
1475
+ return
1476
+ }
1477
+ const close = popover.querySelector(".tab-link-popover-close")
1478
+ if (close && typeof close.focus === "function") {
1479
+ close.focus()
1480
+ } else if (surface && typeof surface.focus === "function") {
1481
+ surface.focus()
1482
+ }
1483
+ })
1484
+ return
1485
+ }
1486
+
1487
+ if (document.body) {
1488
+ document.body.classList.remove("tab-link-popover-open")
1489
+ }
1351
1490
  const rect = link.getBoundingClientRect()
1352
1491
  const minWidth = Math.max(rect.width, 260)
1353
1492
  popover.style.minWidth = `${Math.round(minWidth)}px`
1354
- popover.style.display = "flex"
1493
+ popover.style.display = ""
1355
1494
  popover.classList.add("visible")
1356
1495
  popover.style.visibility = "hidden"
1357
1496
  popover.style.maxHeight = ""
1358
1497
  popover.style.overflowY = ""
1498
+ if (surface) {
1499
+ surface.style.maxHeight = ""
1500
+ surface.style.overflowY = ""
1501
+ }
1359
1502
  const popoverWidth = popover.offsetWidth
1360
1503
  let popoverHeight = popover.offsetHeight
1361
1504
  const viewportPadding = 12
@@ -1380,8 +1523,13 @@
1380
1523
 
1381
1524
  const availableHeight = Math.max(0, window.innerHeight - viewportPadding * 2)
1382
1525
  if (availableHeight > 0 && popoverHeight > availableHeight) {
1383
- popover.style.maxHeight = `${Math.round(availableHeight)}px`
1384
- popover.style.overflowY = "auto"
1526
+ if (surface) {
1527
+ surface.style.maxHeight = `${Math.round(availableHeight)}px`
1528
+ surface.style.overflowY = "auto"
1529
+ } else {
1530
+ popover.style.maxHeight = `${Math.round(availableHeight)}px`
1531
+ popover.style.overflowY = "auto"
1532
+ }
1385
1533
  popoverHeight = Math.min(availableHeight, popover.offsetHeight)
1386
1534
  }
1387
1535
 
@@ -1404,8 +1552,13 @@
1404
1552
 
1405
1553
  const availableBelow = Math.max(0, window.innerHeight - viewportPadding - top)
1406
1554
  if (availableBelow > 0 && popoverHeight > availableBelow) {
1407
- popover.style.maxHeight = `${Math.round(availableBelow)}px`
1408
- popover.style.overflowY = "auto"
1555
+ if (surface) {
1556
+ surface.style.maxHeight = `${Math.round(availableBelow)}px`
1557
+ surface.style.overflowY = "auto"
1558
+ } else {
1559
+ popover.style.maxHeight = `${Math.round(availableBelow)}px`
1560
+ popover.style.overflowY = "auto"
1561
+ }
1409
1562
  popoverHeight = Math.min(availableBelow, popover.offsetHeight)
1410
1563
  }
1411
1564
  }
@@ -1417,14 +1570,27 @@
1417
1570
 
1418
1571
  const hideTabLinkPopover = ({ immediate = false } = {}) => {
1419
1572
  const applyHide = () => {
1573
+ const returnFocusEl = tabLinkPopoverMode === "modal" ? tabLinkReturnFocusEl : null
1420
1574
  if (tabLinkPopoverEl) {
1421
1575
  tabLinkPopoverEl.classList.remove("visible")
1422
1576
  tabLinkPopoverEl.style.display = "none"
1423
1577
  }
1578
+ if (document.body) {
1579
+ document.body.classList.remove("tab-link-popover-open")
1580
+ }
1424
1581
  tabLinkActiveLink = null
1425
1582
  tabLinkPendingLink = null
1426
1583
  tabLinkHideTimer = null
1427
1584
  tabLinkCloseOnMouseLeave = false
1585
+ tabLinkPopoverMode = "anchored"
1586
+ tabLinkReturnFocusEl = null
1587
+ if (returnFocusEl && typeof returnFocusEl.focus === "function" && document.contains(returnFocusEl)) {
1588
+ window.requestAnimationFrame(() => {
1589
+ try {
1590
+ returnFocusEl.focus()
1591
+ } catch (_) {}
1592
+ })
1593
+ }
1428
1594
  }
1429
1595
 
1430
1596
  if (tabLinkHideTimer) {
@@ -1454,6 +1620,10 @@
1454
1620
  const restrictToBase = options && options.restrictToBase === true
1455
1621
  const forceCanonicalQr = options && options.forceCanonicalQr === true
1456
1622
  tabLinkCloseOnMouseLeave = options && options.closeOnMouseLeave === true
1623
+ const presentationMode = tabLinkCloseOnMouseLeave ? "anchored" : "modal"
1624
+ tabLinkReturnFocusEl = presentationMode === "modal"
1625
+ ? ((link && link.querySelector && link.querySelector(`.${TAB_LINK_TRIGGER_CLASS}`)) || link)
1626
+ : null
1457
1627
  let sameOrigin = false
1458
1628
  let canonicalBase = canonicalizeUrl(effectiveHref)
1459
1629
  if (canonicalBase && isHttpUrl(canonicalBase)) {
@@ -1515,10 +1685,11 @@
1515
1685
  // Show lightweight loading popover immediately while mapping fetch runs
1516
1686
  try {
1517
1687
  const pop = ensureTabLinkPopoverEl()
1518
- pop.innerHTML = ''
1519
- const header = document.createElement('div')
1520
- header.className = 'tab-link-popover-header'
1521
- header.innerHTML = `<i class="fa-solid fa-ellipsis"></i><span>Tab actions</span>`
1688
+ const surface = resetTabLinkPopoverContent(pop, presentationMode)
1689
+ if (!surface) {
1690
+ throw new Error("Missing tab link popover surface")
1691
+ }
1692
+ const header = createTabLinkPopoverHeader("Tab actions")
1522
1693
  const item = document.createElement('div')
1523
1694
  item.className = 'tab-link-popover-item tab-link-popover-item--action'
1524
1695
  const icon = document.createElement('span')
@@ -1534,7 +1705,7 @@
1534
1705
  value.textContent = 'Preparing actions'
1535
1706
  copy.append(label, value)
1536
1707
  item.append(icon, copy)
1537
- pop.append(header, item)
1708
+ surface.append(header, item)
1538
1709
  positionTabLinkPopover(pop, link)
1539
1710
  } catch (_) {}
1540
1711
 
@@ -1708,12 +1879,14 @@
1708
1879
  }
1709
1880
 
1710
1881
  const popover = ensureTabLinkPopoverEl()
1711
- popover.innerHTML = ""
1882
+ const popoverSurface = resetTabLinkPopoverContent(popover, presentationMode)
1883
+ if (!popoverSurface) {
1884
+ hideTabLinkPopover({ immediate: true })
1885
+ return
1886
+ }
1712
1887
 
1713
- const header = document.createElement("div")
1714
- header.className = "tab-link-popover-header"
1715
- header.innerHTML = `<i class="fa-solid fa-ellipsis"></i><span>Tab actions</span>`
1716
- popover.appendChild(header)
1888
+ const header = createTabLinkPopoverHeader("Tab actions")
1889
+ popoverSurface.appendChild(header)
1717
1890
 
1718
1891
  actionItems.forEach((actionItem) => {
1719
1892
  const item = document.createElement("button")
@@ -1745,20 +1918,20 @@
1745
1918
  }
1746
1919
  copy.append(labelSpan, valueSpan)
1747
1920
  item.append(icon, copy)
1748
- popover.appendChild(item)
1921
+ popoverSurface.appendChild(item)
1749
1922
  })
1750
1923
 
1751
1924
  if (actionItems.length > 0 && routeEntries.length > 0) {
1752
1925
  const divider = document.createElement("div")
1753
1926
  divider.className = "tab-link-popover-divider"
1754
- popover.appendChild(divider)
1927
+ popoverSurface.appendChild(divider)
1755
1928
  }
1756
1929
 
1757
1930
  if (routeEntries.length > 0) {
1758
1931
  const section = document.createElement("div")
1759
1932
  section.className = "tab-link-popover-section"
1760
1933
  section.textContent = "Browser routes"
1761
- popover.appendChild(section)
1934
+ popoverSurface.appendChild(section)
1762
1935
  }
1763
1936
 
1764
1937
  routeEntries.forEach((entry) => {
@@ -1790,7 +1963,7 @@
1790
1963
  item.className = "tab-link-popover-item"
1791
1964
  item.append(labelSpan, valueSpan)
1792
1965
  }
1793
- popover.appendChild(item)
1966
+ popoverSurface.appendChild(item)
1794
1967
  })
1795
1968
 
1796
1969
  const hasHttpsEntry = Array.isArray(entries) && entries.some((entry) => entry && entry.type === "https")
@@ -1812,7 +1985,7 @@
1812
1985
  footerValue.textContent = "Click to activate"
1813
1986
 
1814
1987
  footerButton.append(footerLabel, footerValue)
1815
- popover.appendChild(footerButton)
1988
+ popoverSurface.appendChild(footerButton)
1816
1989
  }
1817
1990
 
1818
1991
  if (actionItems.length === 0 && routeEntries.length === 0 && !(tabLinkRouterHttpsActive === false && !hasHttpsEntry)) {
@@ -1922,10 +2095,45 @@
1922
2095
  hideTabLinkPopover({ immediate: true })
1923
2096
  }
1924
2097
 
2098
+ const handleGlobalKeydown = (event) => {
2099
+ if (!tabLinkPopoverEl || !tabLinkPopoverEl.classList.contains("visible")) {
2100
+ return
2101
+ }
2102
+ if (event.key === "Escape") {
2103
+ event.preventDefault()
2104
+ hideTabLinkPopover({ immediate: true })
2105
+ return
2106
+ }
2107
+ if (event.key !== "Tab" || tabLinkPopoverMode !== "modal") {
2108
+ return
2109
+ }
2110
+ const surface = getTabLinkPopoverSurface(tabLinkPopoverEl)
2111
+ const focusables = getTabLinkFocusables(surface)
2112
+ if (focusables.length === 0) {
2113
+ event.preventDefault()
2114
+ if (surface && typeof surface.focus === "function") {
2115
+ surface.focus()
2116
+ }
2117
+ return
2118
+ }
2119
+ const first = focusables[0]
2120
+ const last = focusables[focusables.length - 1]
2121
+ if (event.shiftKey && document.activeElement === first) {
2122
+ event.preventDefault()
2123
+ last.focus()
2124
+ } else if (!event.shiftKey && document.activeElement === last) {
2125
+ event.preventDefault()
2126
+ first.focus()
2127
+ }
2128
+ }
2129
+
1925
2130
  window.addEventListener("scroll", (event) => {
1926
2131
  if (!tabLinkPopoverEl || !tabLinkPopoverEl.classList.contains("visible")) {
1927
2132
  return
1928
2133
  }
2134
+ if (tabLinkPopoverMode === "modal") {
2135
+ return
2136
+ }
1929
2137
  if (event && event.target && tabLinkPopoverEl.contains(event.target)) {
1930
2138
  return
1931
2139
  }
@@ -1939,6 +2147,7 @@
1939
2147
  })
1940
2148
 
1941
2149
  document.addEventListener("mousedown", handleGlobalPointer, true)
2150
+ document.addEventListener("keydown", handleGlobalKeydown, true)
1942
2151
  try {
1943
2152
  document.addEventListener("touchstart", handleGlobalPointer, { passive: true, capture: true })
1944
2153
  } catch (_) {