pinokiod 7.3.9 → 7.3.11

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 (52) 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 +52 -88
  15. package/server/public/install.js +20 -2
  16. package/server/public/layout.js +1 -1
  17. package/server/public/nav.js +3 -1
  18. package/server/public/style.css +1455 -521
  19. package/server/public/tab-link-popover.css +162 -18
  20. package/server/public/tab-link-popover.js +230 -21
  21. package/server/public/task-launcher.css +182 -91
  22. package/server/public/terminal-settings.js +227 -50
  23. package/server/public/universal-launcher.css +42 -33
  24. package/server/public/urldropdown.css +284 -0
  25. package/server/views/app.ejs +1718 -352
  26. package/server/views/autolaunch.ejs +4 -5
  27. package/server/views/checkpoints.ejs +223 -50
  28. package/server/views/connect/huggingface.ejs +406 -325
  29. package/server/views/connect.ejs +0 -1
  30. package/server/views/github.ejs +277 -324
  31. package/server/views/index.ejs +65 -8
  32. package/server/views/install.ejs +134 -65
  33. package/server/views/logs.ejs +9 -8
  34. package/server/views/net.ejs +341 -64
  35. package/server/views/network.ejs +85 -63
  36. package/server/views/partials/main_sidebar.ejs +249 -24
  37. package/server/views/plugins.ejs +141 -3
  38. package/server/views/settings.ejs +103 -7
  39. package/server/views/setup.ejs +0 -5
  40. package/server/views/skills.ejs +0 -1
  41. package/server/views/task_list.ejs +0 -1
  42. package/server/views/terminal.ejs +285 -60
  43. package/server/views/terminals.ejs +346 -6
  44. package/server/views/tools.ejs +828 -1691
  45. package/test/caddy-install.test.js +53 -0
  46. package/test/connect-setup.test.js +16 -0
  47. package/test/github-connection.test.js +1 -1
  48. package/test/huggingface-bin.test.js +4 -4
  49. package/test/huggingface-connect.test.js +73 -0
  50. package/test/main-sidebar.test.js +31 -0
  51. package/test/shell-run-template.test.js +5 -1
  52. package/test/uv-bin.test.js +29 -0
@@ -1052,10 +1052,10 @@ body.dark .troubleshoot {
1052
1052
  }
1053
1053
  }
1054
1054
  body.network-page .task-container {
1055
- padding: 20px 28px 32px;
1055
+ padding: 0;
1056
1056
  }
1057
1057
  body.network-page .task-shell {
1058
- width: min(1180px, 100%);
1058
+ width: 100%;
1059
1059
  margin: 0;
1060
1060
  background: transparent;
1061
1061
  border: 0;
@@ -1065,19 +1065,29 @@ body.network-page .task-shell {
1065
1065
  backdrop-filter: none;
1066
1066
  }
1067
1067
  body.network-page .task-shell-header {
1068
- padding-bottom: 18px;
1068
+ padding: 12px 18px 14px;
1069
1069
  }
1070
1070
  body.network-page .task-shell-body {
1071
- padding: 28px 28px 28px;
1071
+ padding: 0 28px 28px;
1072
1072
  }
1073
1073
  body.network-page .c {
1074
1074
  display: grid;
1075
- gap: 24px;
1075
+ gap: 0;
1076
1076
  min-width: 0;
1077
1077
  }
1078
1078
  body.network-page .container {
1079
1079
  margin: 0;
1080
1080
  }
1081
+ body.network-page .network-config-section {
1082
+ padding-top: 18px;
1083
+ border-top: 0;
1084
+ }
1085
+ body.network-page .network-config-head {
1086
+ align-items: center;
1087
+ margin-bottom: 16px;
1088
+ padding-bottom: 16px;
1089
+ border-bottom: 1px solid var(--task-border);
1090
+ }
1081
1091
  body.network-page .config {
1082
1092
  margin: 0;
1083
1093
  padding: 0;
@@ -1088,13 +1098,13 @@ body.network-page .config-header {
1088
1098
  }
1089
1099
  body.network-page .config-body {
1090
1100
  display: grid;
1091
- grid-template-columns: minmax(0, 1fr) auto;
1092
- gap: 14px 18px;
1093
- padding: 18px 22px;
1094
- border: 1px solid var(--task-border);
1095
- border-radius: 18px;
1096
- background: color-mix(in srgb, var(--task-panel) 94%, var(--task-soft));
1097
- align-items: center;
1101
+ grid-template-columns: minmax(0, 1fr);
1102
+ gap: 16px;
1103
+ padding: 0;
1104
+ border: 0;
1105
+ border-radius: 0;
1106
+ background: transparent;
1107
+ align-items: stretch;
1098
1108
  }
1099
1109
  body.network-page .config-row {
1100
1110
  padding: 0;
@@ -1106,15 +1116,21 @@ body.network-page .config-row .label {
1106
1116
  font-weight: 700;
1107
1117
  color: var(--task-text);
1108
1118
  }
1109
- body.network-page .config-toggle-row {
1119
+ body.network-page .config-access-copy {
1120
+ min-width: 0;
1121
+ }
1122
+ body.network-page .config-access-titleline {
1110
1123
  display: flex;
1111
- align-items: center;
1112
- justify-content: space-between;
1113
- gap: 14px;
1124
+ align-items: baseline;
1125
+ gap: 10px;
1126
+ min-width: 0;
1114
1127
  }
1115
- body.network-page .config-toggle-copy {
1116
- display: grid;
1117
- gap: 3px;
1128
+ body.network-page .config-toggle-actions {
1129
+ display: flex;
1130
+ align-items: center;
1131
+ justify-content: flex-end;
1132
+ gap: 12px;
1133
+ flex: 0 0 auto;
1118
1134
  }
1119
1135
  body.network-page #active-label {
1120
1136
  padding: 0;
@@ -1129,18 +1145,11 @@ body.network-page .switcher {
1129
1145
  }
1130
1146
  body.network-page .config-save-btn {
1131
1147
  min-width: 104px;
1132
- min-height: 42px;
1133
- padding: 0 16px;
1134
- border-radius: 12px;
1135
- background: var(--task-accent-surface) !important;
1136
- color: var(--task-accent-contrast) !important;
1137
- border: 1px solid var(--task-accent-surface) !important;
1138
- font-size: 14px;
1139
- font-weight: 700;
1140
- }
1141
- body.network-page .config-save-btn:hover {
1142
- background: var(--task-accent-hover) !important;
1143
- border-color: var(--task-accent-hover) !important;
1148
+ min-height: 32px;
1149
+ padding: 0 12px;
1150
+ border-radius: 7px;
1151
+ font-size: 12px;
1152
+ font-weight: 600;
1144
1153
  }
1145
1154
  body.network-page .slider {
1146
1155
  border: 1px solid var(--task-border-strong);
@@ -1158,7 +1167,8 @@ body.network-page .switcher input:checked + .slider {
1158
1167
  border-color: color-mix(in srgb, var(--task-accent) 72%, var(--task-panel));
1159
1168
  }
1160
1169
  body.network-page #network_name {
1161
- grid-column: 1 / -1;
1170
+ display: grid;
1171
+ gap: 8px;
1162
1172
  }
1163
1173
  body.network-page .network-name {
1164
1174
  display: block;
@@ -1166,22 +1176,22 @@ body.network-page .network-name {
1166
1176
  body.network-page .network-name input[type=text] {
1167
1177
  width: 100%;
1168
1178
  min-width: 0;
1179
+ min-height: 38px;
1180
+ padding: 7px 12px;
1169
1181
  border: 1px solid var(--task-border-strong);
1170
- border-radius: 14px;
1171
- background: color-mix(in srgb, var(--task-panel) 98%, white);
1182
+ border-radius: 9px;
1183
+ background: color-mix(in srgb, var(--task-panel) 92%, var(--task-soft));
1172
1184
  color: var(--task-text);
1173
- padding: 0 14px;
1174
- height: 44px;
1175
1185
  box-sizing: border-box;
1176
1186
  }
1177
1187
  body.dark.network-page .network-name input[type=text] {
1178
- background: color-mix(in srgb, var(--task-panel) 96%, black);
1188
+ background: color-mix(in srgb, var(--task-panel) 92%, var(--task-soft));
1179
1189
  }
1180
1190
  body.network-page #network_name .label {
1181
- margin-bottom: 8px;
1191
+ margin-bottom: 0;
1182
1192
  }
1183
1193
  body.network-page .advanced {
1184
- margin-top: 12px;
1194
+ margin-top: 14px;
1185
1195
  justify-content: flex-start;
1186
1196
  }
1187
1197
  body.network-page .troubleshoot,
@@ -1189,14 +1199,20 @@ body.dark.network-page .troubleshoot {
1189
1199
  color: var(--task-muted);
1190
1200
  padding: 0;
1191
1201
  font-size: 13px;
1202
+ line-height: 1.5;
1203
+ }
1204
+ body.network-page .troubleshoot .link-label {
1205
+ margin-right: 0;
1206
+ padding: 0 !important;
1192
1207
  }
1193
1208
  body.network-page .link-label {
1194
1209
  color: var(--task-accent);
1195
1210
  }
1196
1211
  body.network-page .loading {
1197
- border: 1px solid var(--task-border);
1198
- border-radius: 16px;
1199
- background: color-mix(in srgb, var(--task-panel) 94%, var(--task-soft));
1212
+ margin-top: 16px;
1213
+ border-top: 1px solid var(--task-border);
1214
+ border-radius: 0;
1215
+ background: transparent;
1200
1216
  color: var(--task-muted);
1201
1217
  }
1202
1218
  body.network-page .running-apps {
@@ -1294,12 +1310,8 @@ body.dark.network-page .badge {
1294
1310
  padding-left: 18px;
1295
1311
  padding-right: 18px;
1296
1312
  }
1297
- body.network-page .config-body {
1298
- grid-template-columns: 1fr;
1299
- gap: 16px;
1300
- }
1301
1313
  body.network-page .config-save-btn {
1302
- width: 100%;
1314
+ width: auto;
1303
1315
  }
1304
1316
  }
1305
1317
  @media only screen and (max-width: 560px) {
@@ -1309,6 +1321,12 @@ body.dark.network-page .badge {
1309
1321
  body.network-page .task-shell-body {
1310
1322
  padding-bottom: 22px;
1311
1323
  }
1324
+ body.network-page .network-config-head {
1325
+ align-items: flex-start;
1326
+ }
1327
+ body.network-page .config-toggle-actions {
1328
+ gap: 10px;
1329
+ }
1312
1330
  body.network-page .line.align-top h3 {
1313
1331
  grid-template-columns: 1fr;
1314
1332
  }
@@ -1322,7 +1340,7 @@ body.dark.network-page .badge {
1322
1340
  display: block;
1323
1341
  }
1324
1342
  body.network-page .network-name input[type=text] {
1325
- border-radius: 14px;
1343
+ border-radius: 9px;
1326
1344
  }
1327
1345
  }
1328
1346
  </style>
@@ -1384,31 +1402,35 @@ document.addEventListener('DOMContentLoaded', function() {
1384
1402
  <section class='task-shell network-shell'>
1385
1403
  <header class='task-shell-header'>
1386
1404
  <div class='task-shell-header-main'>
1387
- <p class='task-eyebrow'>Network</p>
1388
1405
  <h1 class='task-title'>Network settings</h1>
1389
1406
  <p class='task-description'>Instantly add HTTPS, CORS, and peer sharing for apps on your local network.</p>
1390
1407
  </div>
1391
1408
  </header>
1392
1409
  <div class='task-shell-body network-shell-body'>
1393
1410
  <div class='c'>
1394
- <div class='container'>
1411
+ <section class='task-section network-config-section' aria-label='Network access'>
1412
+ <div class='task-section-head network-config-head'>
1413
+ <div class='config-access-copy'>
1414
+ <div class='config-access-titleline'>
1415
+ <h2 class='task-section-title'>Network access</h2>
1416
+ <div id='active-label'><%=peer_active ? "On" : "Off"%></div>
1417
+ </div>
1418
+ <p class='task-section-subcopy'>Enable local sharing and set this machine's network name.</p>
1419
+ </div>
1420
+ <div class='config-toggle-actions'>
1421
+ <label class="switcher">
1422
+ <input id='active' type="checkbox" <%=peer_active ? 'checked' : ''%>>
1423
+ <span class="slider"></span>
1424
+ </label>
1425
+ <button class='task-button primary save config-save-btn'>Save</button>
1426
+ </div>
1427
+ </div>
1395
1428
  <div class='config'>
1396
1429
  <div class='config-body'>
1397
- <div class='config-row config-toggle-row'>
1398
- <div class='config-toggle-copy'>
1399
- <div class='label'>Network access</div>
1400
- <div id='active-label'><%=peer_active ? "On" : "Off"%></div>
1401
- </div>
1402
- <label class="switcher">
1403
- <input id='active' type="checkbox" <%=peer_active ? 'checked' : ''%>>
1404
- <span class="slider"></span>
1405
- </label>
1406
- </div>
1407
- <button class='btn save config-save-btn'>Save</button>
1408
1430
  <div class='config-row' id='network_name'>
1409
1431
  <div class='label'>Network Name</div>
1410
1432
  <div class='network-name'>
1411
- <input id='name' type='text' value="<%=name%>">
1433
+ <input id='name' class='task-input' type='text' value="<%=name%>">
1412
1434
  </div>
1413
1435
  </div>
1414
1436
  </div>
@@ -1425,7 +1447,7 @@ document.addEventListener('DOMContentLoaded', function() {
1425
1447
  <div class='loading hidden'>
1426
1448
  <i class="fa-solid fa-circle-notch fa-spin"></i> Stand by. Do not close this window..
1427
1449
  </div>
1428
- </div>
1450
+ </section>
1429
1451
  <div class='container'>
1430
1452
  <div class='running-apps'>
1431
1453
  <% if (requirements_pending || peer_active && processes.length === 0) { %>
@@ -4,6 +4,31 @@
4
4
  const sidebarCurrentHost = (typeof current_host !== "undefined" && current_host) ? current_host : "";
5
5
  const sidebarSelectedNetName = (typeof selectedNetName !== "undefined" && selectedNetName) ? String(selectedNetName) : "";
6
6
  const sidebarShowPeerAccess = typeof showPeerAccess === "boolean" ? showPeerAccess : true;
7
+ const sidebarPeerSourcePoints = Array.isArray(typeof peer_access_points !== "undefined" ? peer_access_points : null) ? peer_access_points : [];
8
+ const sidebarPeerAccessPoints = sidebarPeerSourcePoints
9
+ .filter((point) => point && point.url)
10
+ .map((point) => {
11
+ const url = String(point.url);
12
+ return {
13
+ url,
14
+ qr: point.qr ? String(point.qr) : `/qr?data=${encodeURIComponent(url)}&s=4&m=0`,
15
+ scope_label: point.scope_label ? String(point.scope_label) : "",
16
+ };
17
+ });
18
+ const sidebarPeerAccessRouterInstalled = typeof peer_access_router_installed === "boolean" ? peer_access_router_installed : true;
19
+ if (
20
+ sidebarPeerAccessPoints.length === 0 &&
21
+ typeof peer_qr !== "undefined" && peer_qr &&
22
+ typeof peer_url !== "undefined" && peer_url
23
+ ) {
24
+ sidebarPeerAccessPoints.push({
25
+ url: String(peer_url),
26
+ qr: String(peer_qr),
27
+ scope_label: "",
28
+ });
29
+ }
30
+ const sidebarShowPhoneAccess = sidebarShowPeerAccess && sidebarPeerAccessPoints.length > 0;
31
+ const sidebarPhoneAccessNeedsSetup = sidebarShowPhoneAccess && !sidebarPeerAccessRouterInstalled;
7
32
  const brands = { win32: "windows", darwin: "apple", linux: "Linux" };
8
33
  %>
9
34
  <button type="button" class="main-sidebar-mobile-toggle" data-main-sidebar-toggle aria-label="Open navigation" aria-expanded="false">
@@ -29,44 +54,101 @@
29
54
 
30
55
  <div class='main-sidebar-section' aria-label="Computer">
31
56
  <div class="main-sidebar-section-title">Computer</div>
32
- <a href="/home" class="tab <%= sidebarSelected === 'home' ? 'selected' : '' %>" data-tippy-content="This machine"><i class='fas fa-laptop-code'></i><div class='caption'>This machine</div></a>
33
- <a href="/network" class="tab <%= sidebarSelected === 'network' ? 'selected' : '' %>" data-tippy-content="Local network"><i class="fa-solid fa-wifi"></i><div class='caption'>Local network</div></a>
57
+ <a href="/home" class="tab <%= sidebarSelected === 'home' ? 'selected' : '' %>"><i class='fas fa-laptop-code'></i><div class='caption'>This machine</div></a>
58
+ <a href="/network" class="tab <%= sidebarSelected === 'network' ? 'selected' : '' %>"><i class="fa-solid fa-wifi"></i><div class='caption'>Local network</div></a>
34
59
  <% if (sidebarList.length > 0) { %>
35
60
  <% sidebarList.forEach(({ host, name, platform }) => { %>
36
61
  <% const selectedNet = sidebarSelectedNetName && String(name || "") === sidebarSelectedNetName; %>
37
- <a href="/net/<%=name%>" class="submenu tab <%= selectedNet ? 'selected' : '' %>" data-tippy-content="<%=name%> (<%=sidebarCurrentHost === host ? 'this machine' : host%>)"><i class="fa-brands fa-<%=brands[platform]%>"></i><div class='caption'><%=name%> (<%=sidebarCurrentHost === host ? 'this machine' : host%>)</div></a>
62
+ <a href="/net/<%=name%>" class="submenu tab <%= selectedNet ? 'selected' : '' %>"><i class="fa-brands fa-<%=brands[platform]%>"></i><div class='caption'><%=name%> (<%=sidebarCurrentHost === host ? 'this machine' : host%>)</div></a>
38
63
  <% }) %>
39
64
  <% } %>
65
+ <% if (sidebarShowPhoneAccess) { %>
66
+ <button type="button" class="tab main-sidebar-phone-trigger" data-peer-access-open aria-haspopup="dialog" aria-controls="main-sidebar-peer-dialog" aria-expanded="false">
67
+ <i class="fa-solid fa-mobile-screen-button"></i><div class='caption'>Phone</div>
68
+ </button>
69
+ <% } %>
40
70
  </div>
41
71
 
42
72
  <div class='main-sidebar-section' aria-label="Manage">
43
73
  <div class="main-sidebar-section-title">Manage</div>
44
- <a class="tab <%= sidebarSelected === 'checkpoints' ? 'selected' : '' %>" href="/checkpoints" data-tippy-content="Checkpoints"><i class="fa-solid fa-clock-rotate-left"></i><div class='caption'>Checkpoints</div></a>
45
- <a class="tab <%= sidebarSelected === 'tools' ? 'selected' : '' %>" href="/tools" data-tippy-content="Installed Tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Tools</div></a>
46
- <a class="tab <%= sidebarSelected === 'plugins' ? 'selected' : '' %>" href="/plugins" data-tippy-content="Plugins"><i class="fa-solid fa-plug-circle-bolt"></i><div class='caption'>Plugins</div></a>
47
- <a class="tab <%= sidebarSelected === 'tasks' ? 'selected' : '' %>" href="/tasks" data-tippy-content="Tasks"><i class="fa-solid fa-list-check"></i><div class='caption'>Tasks</div></a>
48
- <a class="tab <%= sidebarSelected === 'skills' ? 'selected' : '' %>" href="/skills" data-tippy-content="Skills"><i class="fa-solid fa-wand-magic-sparkles"></i><div class='caption'>Skills</div></a>
49
- <a class="tab <%= sidebarSelected === 'logs' ? 'selected' : '' %>" id='genlog' href="/logs" data-tippy-content="Logs"><i class="fa-solid fa-laptop-code"></i><div class='caption'>Logs</div></a>
74
+ <a class="tab <%= sidebarSelected === 'checkpoints' ? 'selected' : '' %>" href="/checkpoints"><i class="fa-solid fa-clock-rotate-left"></i><div class='caption'>Checkpoints</div></a>
75
+ <a class="tab <%= sidebarSelected === 'tools' ? 'selected' : '' %>" href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Tools</div></a>
76
+ <a class="tab <%= sidebarSelected === 'plugins' ? 'selected' : '' %>" href="/plugins"><i class="fa-solid fa-plug-circle-bolt"></i><div class='caption'>Plugins</div></a>
77
+ <a class="tab <%= sidebarSelected === 'tasks' ? 'selected' : '' %>" href="/tasks"><i class="fa-solid fa-list-check"></i><div class='caption'>Tasks</div></a>
78
+ <a class="tab <%= sidebarSelected === 'skills' ? 'selected' : '' %>" href="/skills"><i class="fa-solid fa-wand-magic-sparkles"></i><div class='caption'>Skills</div></a>
79
+ <a class="tab <%= sidebarSelected === 'logs' ? 'selected' : '' %>" id='genlog' href="/logs"><i class="fa-solid fa-laptop-code"></i><div class='caption'>Logs</div></a>
50
80
  </div>
51
81
 
52
82
  <div class='main-sidebar-section' aria-label="Configure">
53
83
  <div class="main-sidebar-section-title">Configure</div>
54
- <a class="tab <%= sidebarSelected === 'autolaunch' ? 'selected' : '' %>" href="/autolaunch" data-tippy-content="Autolaunch"><i class="fa-solid fa-power-off"></i><div class='caption'>Autolaunch</div></a>
55
- <a href="/connect" class="tab <%= sidebarSelected === 'connect' ? 'selected' : '' %>" data-tippy-content="Login"><i class="fa-solid fa-plug"></i><div class='caption'>Login</div></a>
56
- <a class="tab <%= sidebarSelected === 'settings' ? 'selected' : '' %>" href="/home?mode=settings" data-tippy-content="Settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
84
+ <a class="tab <%= sidebarSelected === 'autolaunch' ? 'selected' : '' %>" href="/autolaunch"><i class="fa-solid fa-power-off"></i><div class='caption'>Autolaunch</div></a>
85
+ <a href="/connect" class="tab <%= sidebarSelected === 'connect' ? 'selected' : '' %>"><i class="fa-solid fa-plug"></i><div class='caption'>Login</div></a>
86
+ <a class="tab <%= sidebarSelected === 'settings' ? 'selected' : '' %>" href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
57
87
  </div>
58
88
  </nav>
59
-
60
- <% if (sidebarShowPeerAccess) { %>
61
- <div class='main-sidebar-peer'>
62
- <%- include('peer_access_points', {
63
- peer_access_points: (typeof peer_access_points !== "undefined" ? peer_access_points : null),
64
- peer_url: (typeof peer_url !== "undefined" ? peer_url : null),
65
- peer_qr: (typeof peer_qr !== "undefined" ? peer_qr : null),
66
- }) %>
67
- </div>
68
- <% } %>
69
89
  </aside>
90
+ <% if (sidebarShowPhoneAccess) { %>
91
+ <div class="main-sidebar-peer-dialog" data-peer-access-modal hidden>
92
+ <button type="button" class="main-sidebar-peer-dialog-backdrop" data-peer-access-close aria-label="Close phone popup" tabindex="-1"></button>
93
+ <section class="main-sidebar-peer-dialog-surface" id="main-sidebar-peer-dialog" role="dialog" aria-modal="true" aria-labelledby="main-sidebar-peer-dialog-title">
94
+ <div class="main-sidebar-peer-dialog-header">
95
+ <div class="main-sidebar-peer-dialog-icon" aria-hidden="true">
96
+ <i class="fa-solid fa-mobile-screen-button"></i>
97
+ </div>
98
+ <div class="main-sidebar-peer-dialog-heading">
99
+ <div class="main-sidebar-peer-dialog-title" id="main-sidebar-peer-dialog-title"><%= sidebarPhoneAccessNeedsSetup ? 'Set up local access' : 'Open from another device' %></div>
100
+ <div class="main-sidebar-peer-dialog-subtitle"><%= sidebarPhoneAccessNeedsSetup ? 'Same network only' : 'Same Wi-Fi/LAN only' %></div>
101
+ </div>
102
+ <button type="button" class="main-sidebar-peer-dialog-close" data-peer-access-close aria-label="Close phone popup">
103
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
104
+ </button>
105
+ </div>
106
+ <% if (!sidebarPhoneAccessNeedsSetup && sidebarPeerAccessPoints.length > 1) { %>
107
+ <div class="main-sidebar-peer-dialog-tabs" role="tablist" aria-label="Phone connection options">
108
+ <% sidebarPeerAccessPoints.forEach((point, index) => { %>
109
+ <% const tabLabel = point.scope_label || `Option ${index + 1}`; %>
110
+ <button type="button" class="main-sidebar-peer-dialog-tab <%= index === 0 ? 'is-active' : '' %>" data-peer-access-tab data-peer-access-index="<%= index %>" role="tab" aria-selected="<%= index === 0 ? 'true' : 'false' %>" aria-controls="main-sidebar-peer-panel-<%= index %>" tabindex="<%= index === 0 ? '0' : '-1' %>"><%= tabLabel %></button>
111
+ <% }) %>
112
+ </div>
113
+ <% } %>
114
+ <div class="main-sidebar-peer-dialog-body">
115
+ <% if (sidebarPhoneAccessNeedsSetup) { %>
116
+ <div class="main-sidebar-peer-setup">
117
+ <p class="main-sidebar-peer-setup-copy">To open Pinokio from another device on this network, set up local access first.</p>
118
+ <div class="main-sidebar-peer-setup-actions">
119
+ <a class="main-sidebar-peer-setup-primary" href="/setup/network" data-peer-access-setup>
120
+ Set up local access
121
+ </a>
122
+ <button type="button" class="main-sidebar-peer-setup-secondary" data-peer-access-close>
123
+ Cancel
124
+ </button>
125
+ </div>
126
+ </div>
127
+ <% } else { %>
128
+ <% sidebarPeerAccessPoints.forEach((point, index) => { %>
129
+ <div class="main-sidebar-peer-dialog-panel <%= index === 0 ? 'is-active' : '' %>" id="main-sidebar-peer-panel-<%= index %>" data-peer-access-panel data-peer-access-index="<%= index %>" role="tabpanel" <%= index === 0 ? '' : 'hidden' %>>
130
+ <a class="main-sidebar-peer-qr-link" href="<%= point.url %>" target="_blank" rel="noreferrer">
131
+ <img class="main-sidebar-peer-qr-image" src="<%= point.qr %>" alt="QR code for <%= point.url %>"/>
132
+ </a>
133
+ <div class="main-sidebar-peer-caption">
134
+ Scan with a device on the same local network
135
+ </div>
136
+ <div class="main-sidebar-peer-local-note">
137
+ This link is not public and will not work outside this network.
138
+ </div>
139
+ <div class="main-sidebar-peer-url-row">
140
+ <a class="main-sidebar-peer-url" href="<%= point.url %>" target="_blank" rel="noreferrer"><%= point.url %></a>
141
+ <button type="button" class="main-sidebar-peer-copy" data-peer-access-copy data-peer-access-url="<%= point.url %>" aria-label="Copy phone URL" title="Copy URL">
142
+ <i class="fa-regular fa-copy" aria-hidden="true"></i>
143
+ </button>
144
+ </div>
145
+ </div>
146
+ <% }) %>
147
+ <% } %>
148
+ </div>
149
+ </section>
150
+ </div>
151
+ <% } %>
70
152
  <script>
71
153
  (() => {
72
154
  const body = document.body;
@@ -123,19 +205,148 @@
123
205
  };
124
206
  }
125
207
 
208
+ if (window.__pinokioPeerAccessModalInit) return;
209
+ window.__pinokioPeerAccessModalInit = true;
210
+
211
+ const peerAccessTrigger = document.querySelector("[data-peer-access-open]");
212
+ const peerAccessModal = document.querySelector("[data-peer-access-modal]");
213
+ if (body && peerAccessTrigger && peerAccessModal) {
214
+ const peerAccessCloseButtons = peerAccessModal.querySelectorAll("[data-peer-access-close]");
215
+ const peerAccessTabs = peerAccessModal.querySelectorAll("[data-peer-access-tab]");
216
+ const peerAccessPanels = peerAccessModal.querySelectorAll("[data-peer-access-panel]");
217
+ const peerAccessCopyButtons = peerAccessModal.querySelectorAll("[data-peer-access-copy]");
218
+ const peerAccessSetupLinks = peerAccessModal.querySelectorAll("[data-peer-access-setup]");
219
+ const peerAccessFocusTarget = peerAccessModal.querySelector(".main-sidebar-peer-dialog-close");
220
+
221
+ const setPeerAccessOpen = (open) => {
222
+ const nextOpen = !!open;
223
+ peerAccessModal.hidden = !nextOpen;
224
+ body.classList.toggle("main-sidebar-peer-dialog-open", nextOpen);
225
+ peerAccessTrigger.setAttribute("aria-expanded", nextOpen ? "true" : "false");
226
+ if (nextOpen) {
227
+ window.requestAnimationFrame(() => {
228
+ if (peerAccessFocusTarget && typeof peerAccessFocusTarget.focus === "function") {
229
+ peerAccessFocusTarget.focus();
230
+ }
231
+ });
232
+ } else if (typeof peerAccessTrigger.focus === "function") {
233
+ peerAccessTrigger.focus();
234
+ }
235
+ };
236
+
237
+ const setPeerAccessPanel = (index) => {
238
+ const activeIndex = String(index);
239
+ peerAccessTabs.forEach((tab) => {
240
+ const active = tab.dataset.peerAccessIndex === activeIndex;
241
+ tab.classList.toggle("is-active", active);
242
+ tab.setAttribute("aria-selected", active ? "true" : "false");
243
+ tab.setAttribute("tabindex", active ? "0" : "-1");
244
+ });
245
+ peerAccessPanels.forEach((panel) => {
246
+ const active = panel.dataset.peerAccessIndex === activeIndex;
247
+ panel.hidden = !active;
248
+ panel.classList.toggle("is-active", active);
249
+ });
250
+ };
251
+
252
+ const copyText = async (text) => {
253
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
254
+ await navigator.clipboard.writeText(text);
255
+ return;
256
+ }
257
+ const textarea = document.createElement("textarea");
258
+ textarea.value = text;
259
+ textarea.setAttribute("readonly", "");
260
+ textarea.style.position = "fixed";
261
+ textarea.style.opacity = "0";
262
+ document.body.appendChild(textarea);
263
+ textarea.select();
264
+ document.execCommand("copy");
265
+ textarea.remove();
266
+ };
267
+
268
+ peerAccessTrigger.addEventListener("click", () => {
269
+ setPeerAccessOpen(peerAccessModal.hidden);
270
+ });
271
+
272
+ peerAccessCloseButtons.forEach((button) => {
273
+ button.addEventListener("click", () => setPeerAccessOpen(false));
274
+ });
275
+
276
+ peerAccessTabs.forEach((tab) => {
277
+ tab.addEventListener("click", () => setPeerAccessPanel(tab.dataset.peerAccessIndex || "0"));
278
+ });
279
+
280
+ peerAccessSetupLinks.forEach((link) => {
281
+ link.addEventListener("click", (event) => {
282
+ event.preventDefault();
283
+ const currentPath = `${window.location.pathname}${window.location.search}`;
284
+ window.location.href = `/setup/network?callback=${encodeURIComponent(currentPath || "/")}`;
285
+ });
286
+ });
287
+
288
+ peerAccessCopyButtons.forEach((button) => {
289
+ button.addEventListener("click", async () => {
290
+ const url = button.dataset.peerAccessUrl || "";
291
+ if (!url) return;
292
+ try {
293
+ await copyText(url);
294
+ const icon = button.querySelector("i");
295
+ button.setAttribute("aria-label", "Copied phone URL");
296
+ button.setAttribute("title", "Copied");
297
+ if (icon) {
298
+ icon.classList.remove("fa-regular", "fa-copy");
299
+ icon.classList.add("fa-solid", "fa-check");
300
+ }
301
+ window.setTimeout(() => {
302
+ button.setAttribute("aria-label", "Copy phone URL");
303
+ button.setAttribute("title", "Copy URL");
304
+ if (icon) {
305
+ icon.classList.remove("fa-solid", "fa-check");
306
+ icon.classList.add("fa-regular", "fa-copy");
307
+ }
308
+ }, 1200);
309
+ } catch (_) {}
310
+ });
311
+ });
312
+
313
+ document.addEventListener("keydown", (event) => {
314
+ if (event.key === "Escape" && !peerAccessModal.hidden) {
315
+ setPeerAccessOpen(false);
316
+ }
317
+ });
318
+ }
319
+
126
320
  if (window.__pinokioMainSidebarMobileInit) return;
127
321
  window.__pinokioMainSidebarMobileInit = true;
128
322
 
129
323
  const html = document.documentElement;
130
324
  const toggle = document.querySelector("[data-main-sidebar-toggle]");
325
+ const mobileHomeToggle = document.querySelector("body.main-sidebar-page > header.navheader .home");
131
326
  const backdrop = document.querySelector("[data-main-sidebar-backdrop]");
132
- const mobileMq = window.matchMedia("(max-width: 600px)");
327
+ const mobileMq = window.matchMedia("(max-width: 768px)");
133
328
 
134
329
  if (!body || !html || !sidebar || !toggle || !backdrop) return;
135
330
 
136
331
  const sync = () => {
137
332
  const open = body.classList.contains("main-sidebar-mobile-open");
138
333
  toggle.setAttribute("aria-expanded", open ? "true" : "false");
334
+ if (mobileHomeToggle) {
335
+ if (mobileMq.matches) {
336
+ const label = open ? "Close navigation" : "Open navigation";
337
+ mobileHomeToggle.setAttribute("role", "button");
338
+ mobileHomeToggle.setAttribute("aria-controls", "main-sidebar");
339
+ mobileHomeToggle.setAttribute("aria-expanded", open ? "true" : "false");
340
+ mobileHomeToggle.setAttribute("aria-label", label);
341
+ mobileHomeToggle.setAttribute("title", label);
342
+ } else {
343
+ mobileHomeToggle.removeAttribute("role");
344
+ mobileHomeToggle.removeAttribute("aria-controls");
345
+ mobileHomeToggle.removeAttribute("aria-expanded");
346
+ mobileHomeToggle.removeAttribute("aria-label");
347
+ mobileHomeToggle.removeAttribute("title");
348
+ }
349
+ }
139
350
  const icon = toggle.querySelector("i");
140
351
  if (icon) {
141
352
  icon.classList.toggle("fa-bars", !open);
@@ -157,14 +368,28 @@
157
368
  sync();
158
369
  };
159
370
 
160
- toggle.addEventListener("click", () => {
371
+ const toggleMobileSidebar = () => {
161
372
  if (body.classList.contains("main-sidebar-mobile-open")) {
162
373
  close();
163
374
  } else {
164
375
  open();
165
376
  }
377
+ };
378
+
379
+ toggle.addEventListener("click", () => {
380
+ toggleMobileSidebar();
166
381
  });
167
382
 
383
+ if (mobileHomeToggle) {
384
+ mobileHomeToggle.addEventListener("click", (event) => {
385
+ if (!mobileMq.matches) {
386
+ return;
387
+ }
388
+ event.preventDefault();
389
+ toggleMobileSidebar();
390
+ });
391
+ }
392
+
168
393
  backdrop.addEventListener("click", close);
169
394
 
170
395
  sidebar.addEventListener("click", (event) => {