pinokiod 5.1.10 → 5.1.17

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 (56) hide show
  1. package/kernel/api/fs/download_worker.js +158 -0
  2. package/kernel/api/fs/index.js +95 -91
  3. package/kernel/api/index.js +3 -0
  4. package/kernel/bin/index.js +5 -2
  5. package/kernel/environment.js +19 -2
  6. package/kernel/git.js +972 -1
  7. package/kernel/index.js +65 -30
  8. package/kernel/peer.js +1 -2
  9. package/kernel/plugin.js +0 -8
  10. package/kernel/procs.js +92 -36
  11. package/kernel/prototype.js +45 -22
  12. package/kernel/shells.js +30 -6
  13. package/kernel/sysinfo.js +33 -13
  14. package/kernel/util.js +61 -24
  15. package/kernel/workspace_status.js +131 -7
  16. package/package.json +1 -1
  17. package/pipe/index.js +1 -1
  18. package/server/index.js +1169 -350
  19. package/server/public/create-launcher.js +157 -2
  20. package/server/public/install.js +135 -41
  21. package/server/public/style.css +32 -1
  22. package/server/public/tab-link-popover.js +45 -14
  23. package/server/public/terminal-settings.js +51 -35
  24. package/server/public/urldropdown.css +89 -3
  25. package/server/socket.js +12 -7
  26. package/server/views/agents.ejs +4 -3
  27. package/server/views/app.ejs +798 -30
  28. package/server/views/bootstrap.ejs +2 -1
  29. package/server/views/checkpoints.ejs +1014 -0
  30. package/server/views/checkpoints_registry_beta.ejs +260 -0
  31. package/server/views/columns.ejs +4 -4
  32. package/server/views/connect.ejs +1 -0
  33. package/server/views/d.ejs +74 -4
  34. package/server/views/download.ejs +28 -28
  35. package/server/views/editor.ejs +4 -5
  36. package/server/views/env_editor.ejs +1 -1
  37. package/server/views/file_explorer.ejs +1 -1
  38. package/server/views/index.ejs +3 -1
  39. package/server/views/init/index.ejs +2 -1
  40. package/server/views/install.ejs +2 -1
  41. package/server/views/net.ejs +9 -7
  42. package/server/views/network.ejs +15 -14
  43. package/server/views/pro.ejs +5 -2
  44. package/server/views/prototype/index.ejs +2 -1
  45. package/server/views/registry_link.ejs +76 -0
  46. package/server/views/rows.ejs +4 -4
  47. package/server/views/screenshots.ejs +1 -0
  48. package/server/views/settings.ejs +1 -0
  49. package/server/views/shell.ejs +4 -6
  50. package/server/views/terminal.ejs +528 -38
  51. package/server/views/tools.ejs +1 -0
  52. package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/1764297248545 +0 -45
  53. package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/1764335557118 +0 -45
  54. package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/1764335834126 +0 -45
  55. package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/events +0 -12
  56. package/undefined/logs/dev/plugin/cursor-agent.git/pinokio.js/latest +0 -45
@@ -364,6 +364,106 @@
364
364
  return { syncTemplateFields, getTemplateValues, setTemplateValues };
365
365
  }
366
366
 
367
+ function buildAttachmentSection() {
368
+ const wrapper = document.createElement('div');
369
+ wrapper.className = 'create-launcher-upload';
370
+
371
+ const label = document.createElement('div');
372
+ label.className = 'create-launcher-upload-label';
373
+ label.textContent = 'Attach files (optional)';
374
+
375
+ const dropzone = document.createElement('div');
376
+ dropzone.className = 'create-launcher-upload-dropzone';
377
+ dropzone.textContent = 'Drag and drop files here, or click to select';
378
+
379
+ const input = document.createElement('input');
380
+ input.type = 'file';
381
+ input.multiple = true;
382
+ input.style.display = 'none';
383
+
384
+ const list = document.createElement('ul');
385
+ list.className = 'create-launcher-upload-list';
386
+
387
+ let files = [];
388
+
389
+ function updateList() {
390
+ list.innerHTML = '';
391
+ if (!files.length) {
392
+ const empty = document.createElement('li');
393
+ empty.className = 'create-launcher-upload-empty';
394
+ empty.textContent = 'No files selected';
395
+ list.appendChild(empty);
396
+ return;
397
+ }
398
+ files.forEach((file, index) => {
399
+ const item = document.createElement('li');
400
+ item.className = 'create-launcher-upload-item';
401
+ const name = document.createElement('span');
402
+ name.className = 'create-launcher-upload-name';
403
+ name.textContent = file.name;
404
+ const size = document.createElement('span');
405
+ size.className = 'create-launcher-upload-size';
406
+ size.textContent = `${Math.max(1, Math.ceil(file.size / 1024))} KB`;
407
+ const remove = document.createElement('button');
408
+ remove.type = 'button';
409
+ remove.className = 'create-launcher-upload-remove';
410
+ remove.textContent = 'Remove';
411
+ remove.addEventListener('click', () => {
412
+ files = files.filter((_, i) => i !== index);
413
+ updateList();
414
+ });
415
+ item.appendChild(name);
416
+ item.appendChild(size);
417
+ item.appendChild(remove);
418
+ list.appendChild(item);
419
+ });
420
+ }
421
+
422
+ function addFiles(fileList) {
423
+ const incoming = Array.from(fileList || []);
424
+ if (incoming.length) {
425
+ files = files.concat(incoming);
426
+ updateList();
427
+ }
428
+ }
429
+
430
+ dropzone.addEventListener('click', () => input.click());
431
+ dropzone.addEventListener('dragover', (event) => {
432
+ event.preventDefault();
433
+ dropzone.classList.add('dragover');
434
+ });
435
+ dropzone.addEventListener('dragleave', () => {
436
+ dropzone.classList.remove('dragover');
437
+ });
438
+ dropzone.addEventListener('drop', (event) => {
439
+ event.preventDefault();
440
+ dropzone.classList.remove('dragover');
441
+ addFiles(event.dataTransfer ? event.dataTransfer.files : []);
442
+ });
443
+ input.addEventListener('change', (event) => {
444
+ addFiles(event.target.files);
445
+ input.value = '';
446
+ });
447
+
448
+ updateList();
449
+
450
+ wrapper.appendChild(label);
451
+ wrapper.appendChild(dropzone);
452
+ wrapper.appendChild(list);
453
+ wrapper.appendChild(input);
454
+
455
+ return {
456
+ wrapper,
457
+ getFiles() {
458
+ return files.slice();
459
+ },
460
+ clear() {
461
+ files = [];
462
+ updateList();
463
+ }
464
+ };
465
+ }
466
+
367
467
  function buildCreateLauncherUI({ mode = 'modal', tools }) {
368
468
  const isPage = mode === 'page';
369
469
  const overlay = isPage ? null : document.createElement('div');
@@ -453,6 +553,8 @@
453
553
  folderInput.className = 'create-launcher-modal-input';
454
554
  folderLabel.appendChild(folderInput);
455
555
 
556
+ const attachments = buildAttachmentSection();
557
+
456
558
  const { wrapper: toolWrapper, toolEntries } = buildToolOptions(tools);
457
559
 
458
560
  const error = document.createElement('div');
@@ -495,6 +597,7 @@
495
597
  container.appendChild(promptLabel);
496
598
  container.appendChild(templateWrapper);
497
599
  container.appendChild(folderLabel);
600
+ container.appendChild(attachments.wrapper);
498
601
  container.appendChild(toolWrapper);
499
602
  container.appendChild(error);
500
603
  container.appendChild(actions);
@@ -539,6 +642,7 @@
539
642
  advancedLink,
540
643
  bookmarkletLink,
541
644
  linkRow,
645
+ attachments,
542
646
  currentVariant: MODAL_VARIANTS.CREATE,
543
647
  projectName: '',
544
648
  resetFolderTracking() {
@@ -570,6 +674,9 @@
570
674
  if (ui.folderLabel) {
571
675
  ui.folderLabel.style.display = isAsk ? 'none' : '';
572
676
  }
677
+ if (ui.attachments && ui.attachments.wrapper) {
678
+ ui.attachments.wrapper.style.display = isAsk ? 'none' : '';
679
+ }
573
680
  if (ui.linkRow) {
574
681
  ui.linkRow.style.display = isAsk ? 'none' : '';
575
682
  }
@@ -624,6 +731,31 @@
624
731
  return ui && ui.templateManager ? ui.templateManager.getTemplateValues() : new Map();
625
732
  }
626
733
 
734
+ async function uploadAttachments(ui, files) {
735
+ if (!files || !files.length) {
736
+ return null;
737
+ }
738
+ const formData = new FormData();
739
+ files.forEach((file) => {
740
+ if (file) {
741
+ formData.append('files', file, file.name || 'file');
742
+ }
743
+ });
744
+ const response = await fetch('/create-upload', {
745
+ method: 'POST',
746
+ body: formData
747
+ });
748
+ if (!response.ok) {
749
+ const text = await response.text();
750
+ throw new Error(text || 'Failed to upload files.');
751
+ }
752
+ const data = await response.json();
753
+ if (data && data.error) {
754
+ throw new Error(data.error);
755
+ }
756
+ return data;
757
+ }
758
+
627
759
  async function submitFromUi(ui) {
628
760
  if (!ui) return;
629
761
  ui.error.textContent = '';
@@ -639,6 +771,10 @@
639
771
  : null
640
772
  const selectedTool = selectedEntry && selectedEntry.input ? selectedEntry.input.value : ''
641
773
  const selectedHref = selectedEntry && selectedEntry.input ? selectedEntry.input.dataset.agentHref : ''
774
+ const selectedFiles = ui.attachments && typeof ui.attachments.getFiles === 'function'
775
+ ? ui.attachments.getFiles()
776
+ : [];
777
+ let uploadToken = '';
642
778
 
643
779
  if (!selectedEntry || !selectedHref) {
644
780
  ui.error.textContent = 'Please select an agent.';
@@ -682,6 +818,18 @@
682
818
  finalPrompt = applyTemplateValues(rawPrompt, templateValues);
683
819
  }
684
820
 
821
+ if (selectedFiles.length > 0) {
822
+ try {
823
+ const uploadResult = await uploadAttachments(ui, selectedFiles);
824
+ if (uploadResult && uploadResult.uploadToken) {
825
+ uploadToken = uploadResult.uploadToken;
826
+ }
827
+ } catch (uploadError) {
828
+ ui.error.textContent = uploadError.message || 'Failed to upload files.';
829
+ return;
830
+ }
831
+ }
832
+
685
833
  const prompt = finalPrompt.trim();
686
834
 
687
835
  if (isAskVariant) {
@@ -701,6 +849,9 @@
701
849
  if (selectedTool) {
702
850
  params.set('tool', selectedTool);
703
851
  }
852
+ if (uploadToken) {
853
+ params.set('uploadToken', uploadToken);
854
+ }
704
855
 
705
856
  window.location.href = `/pro?${params.toString()}`;
706
857
  }
@@ -719,7 +870,9 @@
719
870
 
720
871
  document.body.appendChild(ui.overlay);
721
872
 
722
- ui.confirmButton.addEventListener('click', () => submitFromUi(ui));
873
+ ui.confirmButton.addEventListener('click', () => {
874
+ submitFromUi(ui);
875
+ });
723
876
  if (ui.cancelButton) {
724
877
  ui.cancelButton.addEventListener('click', hideModal);
725
878
  }
@@ -791,7 +944,9 @@
791
944
  root.innerHTML = '';
792
945
  root.appendChild(ui.container);
793
946
 
794
- ui.confirmButton.addEventListener('click', () => submitFromUi(ui));
947
+ ui.confirmButton.addEventListener('click', () => {
948
+ submitFromUi(ui);
949
+ });
795
950
 
796
951
  applyDefaultsToUi(ui, defaults);
797
952
  ui.templateManager.syncTemplateFields(ui.promptTextarea.value, defaults.templateValues || {});
@@ -1,4 +1,4 @@
1
- const installname = async (url, name) => {
1
+ const installname = async (url, name, options) => {
2
2
  if (url.startsWith("http")) {
3
3
  let urlChunks = new URL(url).pathname.split("/")
4
4
  let defaultName = urlChunks[urlChunks.length-1]
@@ -13,6 +13,7 @@ const installname = async (url, name) => {
13
13
  // showCancelButton: true,
14
14
  confirmButtonText: 'Download',
15
15
  allowOutsideClick: false,
16
+ showLoaderOnConfirm: true,
16
17
  didOpen: () => {
17
18
  let input = Swal.getPopup().querySelector('#swal-input1')
18
19
  if (name) {
@@ -21,16 +22,31 @@ const installname = async (url, name) => {
21
22
  input.value = defaultName;
22
23
  }
23
24
  input.addEventListener("keypress", (e) => {
24
- if (event.key === "Enter") {
25
+ if (e.key === "Enter") {
25
26
  e.preventDefault()
26
27
  e.stopPropagation()
27
28
  Swal.clickConfirm()
28
29
  }
29
30
  })
30
31
  },
31
- preConfirm: () => {
32
- const name = Swal.getPopup().querySelector('#swal-input1').value;
33
- return name
32
+ preConfirm: async () => {
33
+ const folderName = (Swal.getPopup().querySelector("#swal-input1").value || "").trim()
34
+ const validationError = validateInstallFolderName(folderName)
35
+ if (validationError) {
36
+ Swal.showValidationMessage(validationError)
37
+ return false
38
+ }
39
+ try {
40
+ const exists = await checkInstallDestinationExists(folderName, options)
41
+ if (exists) {
42
+ Swal.showValidationMessage("Folder already exists. Choose a different name.")
43
+ return false
44
+ }
45
+ } catch (error) {
46
+ Swal.showValidationMessage(error && error.message ? error.message : "Could not check destination folder")
47
+ return false
48
+ }
49
+ return folderName
34
50
  }
35
51
  })
36
52
  return result.value
@@ -64,53 +80,130 @@ const normalizeInstallPath = (rawPath) => {
64
80
  return segments.join('/')
65
81
  }
66
82
 
83
+ const validateInstallFolderName = (folderName) => {
84
+ if (!folderName) {
85
+ return "Name is required"
86
+ }
87
+ if (folderName === "." || folderName === "..") {
88
+ return "Invalid name"
89
+ }
90
+ if (/[\\/]/.test(folderName)) {
91
+ return "Name cannot include / or \\\\"
92
+ }
93
+ if (folderName.includes("\0")) {
94
+ return "Invalid name"
95
+ }
96
+ return null
97
+ }
98
+
99
+ const checkInstallDestinationExists = async (folderName, options) => {
100
+ const normalizedPath = options && options.path ? normalizeInstallPath(options.path) : null
101
+ const relativePath = normalizedPath || DEFAULT_INSTALL_RELATIVE_PATH
102
+ const response = await fetch("/pinokio/install/exists", {
103
+ method: "POST",
104
+ headers: {
105
+ "Content-Type": "application/json"
106
+ },
107
+ body: JSON.stringify({
108
+ relativePath,
109
+ folderName
110
+ })
111
+ })
112
+ const payload = await response.json().catch(() => ({}))
113
+ if (!response.ok) {
114
+ const message = payload && payload.error ? payload.error : `Failed to check destination folder (${response.status})`
115
+ throw new Error(message)
116
+ }
117
+ return payload && payload.exists === true
118
+ }
119
+
67
120
  const install = async (name, url, term, socket, options) => {
68
121
  console.log("options", options)
69
122
  const n = new N()
70
123
  const normalizedPath = options && options.path ? normalizeInstallPath(options.path) : null
71
124
  const targetPath = normalizedPath ? `~/${normalizedPath}` : `~/${DEFAULT_INSTALL_RELATIVE_PATH}`
72
125
 
73
- await new Promise((resolve, reject) => {
74
- socket.close()
75
-
76
- // normalize git url to the standard .git format
77
- let branch
78
- if (options && options.branch) {
79
- branch = options.branch
126
+ try {
127
+ const exists = await checkInstallDestinationExists(name, options)
128
+ if (exists) {
129
+ n.Noty({
130
+ text: "Folder already exists. Choose a different name.",
131
+ timeout: 6000
132
+ })
133
+ return
80
134
  }
135
+ } catch (error) {
136
+ n.Noty({
137
+ text: error && error.message ? error.message : "Could not verify destination folder",
138
+ timeout: 6000
139
+ })
140
+ return
141
+ }
81
142
 
82
- if (!url.endsWith(".git")) {
83
- url = url + ".git"
84
- }
143
+ try {
144
+ await new Promise((resolve, reject) => {
145
+ let settled = false
146
+ const settle = (fn, value) => {
147
+ if (settled) return
148
+ settled = true
149
+ fn(value)
150
+ }
151
+ socket.close()
85
152
 
86
- let cmd
87
- if (branch) {
88
- cmd = `git clone -b ${branch} ${url} ${name}`
89
- } else {
90
- cmd = `git clone ${url} ${name}`
91
- }
92
- socket.run({
93
- method: "shell.run",
94
- client: {
95
- cols: term.cols,
96
- rows: term.rows,
97
- },
98
- params: {
99
- message: cmd,
100
- path: targetPath
153
+ // normalize git url to the standard .git format
154
+ let branch
155
+ if (options && options.branch) {
156
+ branch = options.branch
101
157
  }
102
- }, (packet) => {
103
- if (packet.type === 'stream') {
104
- term.write(packet.data.raw)
105
- } else if (packet.type === "result") {
106
- resolve()
107
- } else if (packet.type === "error") {
108
- n.Noty({
109
- text: packet.data
110
- })
158
+
159
+ if (!url.endsWith(".git")) {
160
+ url = url + ".git"
161
+ }
162
+
163
+ let cmd
164
+ if (branch) {
165
+ cmd = `git clone -b ${branch} ${url} ${name}`
166
+ } else {
167
+ cmd = `git clone ${url} ${name}`
111
168
  }
169
+ socket.run({
170
+ method: "shell.run",
171
+ client: {
172
+ cols: term.cols,
173
+ rows: term.rows,
174
+ },
175
+ params: {
176
+ message: cmd,
177
+ path: targetPath,
178
+ on: [{
179
+ event: "/fatal:/i",
180
+ break: true
181
+ }]
182
+ }
183
+ }, (packet) => {
184
+ if (packet.type === 'stream') {
185
+ term.write(packet.data.raw)
186
+ } else if (packet.type === "result") {
187
+ if (packet.data && packet.data.error && packet.data.error.length > 0) {
188
+ n.Noty({
189
+ text: "Download failed. See terminal output for details.",
190
+ timeout: 6000
191
+ })
192
+ settle(reject, new Error("shell.run failed"))
193
+ return
194
+ }
195
+ settle(resolve)
196
+ } else if (packet.type === "error") {
197
+ n.Noty({
198
+ text: packet.data
199
+ })
200
+ settle(reject, new Error(typeof packet.data === "string" ? packet.data : "shell.run error"))
201
+ }
202
+ })
112
203
  })
113
- })
204
+ } catch (_) {
205
+ return
206
+ }
114
207
  /*
115
208
  options := {
116
209
  html,
@@ -158,7 +251,8 @@ const createTerm = async (_theme) => {
158
251
  })
159
252
  let config = {
160
253
  scrollback: 9999999,
161
- fontSize: 12,
254
+ fontSize: 14,
255
+ fontFamily: 'monospace',
162
256
  theme,
163
257
  }
164
258
  let res = await fetch("/xterm_config").then((res) => {
@@ -2106,6 +2106,25 @@ body.dark .swal2-title {
2106
2106
  background: rgba(0,0,0,0.8) !important;
2107
2107
  */
2108
2108
  }
2109
+ .swal2-validation-message {
2110
+ margin: 12px 15px 0;
2111
+ padding: 10px 12px;
2112
+ border-radius: 6px;
2113
+ background: rgba(255, 66, 66, 0.12);
2114
+ border: 1px solid rgba(255, 66, 66, 0.18);
2115
+ border-left: 4px solid rgba(255, 66, 66, 0.55);
2116
+ color: rgba(0,0,0,0.85);
2117
+ text-align: left;
2118
+ font-size: 13px;
2119
+ line-height: 1.35;
2120
+ box-sizing: border-box;
2121
+ }
2122
+ body.dark .swal2-validation-message {
2123
+ background: rgba(255, 66, 66, 0.16);
2124
+ border-color: rgba(255, 66, 66, 0.25);
2125
+ border-left-color: rgba(255, 66, 66, 0.75);
2126
+ color: rgba(255,255,255,0.9);
2127
+ }
2109
2128
  .swal2-modal button.swal2-cancel {
2110
2129
  background: none !important;
2111
2130
  }
@@ -2134,6 +2153,12 @@ body.dark .swal2-modal input {
2134
2153
  color: var(--dark-btn-color);
2135
2154
  border: 2px solid rgba(255,255,255,0.1);
2136
2155
  }
2156
+ .swal2-modal input.swal2-inputerror {
2157
+ border-color: rgba(255, 66, 66, 0.6);
2158
+ }
2159
+ body.dark .swal2-modal input.swal2-inputerror {
2160
+ border-color: rgba(255, 66, 66, 0.55);
2161
+ }
2137
2162
  .swal2-modal input {
2138
2163
  /*
2139
2164
  background: rgba(0,0,0,0.1) !important;
@@ -2765,7 +2790,7 @@ body.dark .mode-selector .btn2.selected {
2765
2790
  }
2766
2791
  .mode-selector .btn2 {
2767
2792
  display: flex;
2768
- align-items: center;
2793
+ align-items: stretch;
2769
2794
  flex-direction: row;
2770
2795
  justify-content: center;
2771
2796
  padding: 5px 10px;
@@ -3508,6 +3533,12 @@ body.dark .logs-viewer-output {
3508
3533
  #status-window {
3509
3534
  display: none !important;
3510
3535
  }
3536
+ .snapshot-btn-save {
3537
+ background: #7f5bf3 !important;
3538
+ }
3539
+ .snapshot-footer-actions .btn-primary {
3540
+ background: #7f5bf3 !important;
3541
+ }
3511
3542
 
3512
3543
  @media (max-width: 800px) {
3513
3544
  .logs-page {
@@ -1239,27 +1239,58 @@
1239
1239
  popover.style.visibility = "hidden"
1240
1240
  popover.style.maxHeight = ""
1241
1241
  popover.style.overflowY = ""
1242
-
1243
1242
  const popoverWidth = popover.offsetWidth
1244
1243
  let popoverHeight = popover.offsetHeight
1245
1244
  const viewportPadding = 12
1246
1245
  const dropOffset = 8
1247
1246
 
1248
- let left = rect.left
1249
- const top = Math.max(viewportPadding, rect.bottom + dropOffset)
1247
+ const appcanvas = document.querySelector(".appcanvas")
1248
+ const isVerticalLayout = !!(appcanvas && appcanvas.classList.contains("vertical"))
1250
1249
 
1251
- if (left + popoverWidth > window.innerWidth - viewportPadding) {
1252
- left = window.innerWidth - popoverWidth - viewportPadding
1253
- }
1254
- if (left < viewportPadding) {
1255
- left = viewportPadding
1256
- }
1250
+ let left
1251
+ let top
1252
+
1253
+ if (isVerticalLayout) {
1254
+ left = rect.right + dropOffset
1255
+ top = rect.top
1256
+
1257
+ if (left + popoverWidth > window.innerWidth - viewportPadding) {
1258
+ left = window.innerWidth - popoverWidth - viewportPadding
1259
+ }
1260
+ if (left < viewportPadding) {
1261
+ left = viewportPadding
1262
+ }
1263
+
1264
+ const availableHeight = Math.max(0, window.innerHeight - viewportPadding * 2)
1265
+ if (availableHeight > 0 && popoverHeight > availableHeight) {
1266
+ popover.style.maxHeight = `${Math.round(availableHeight)}px`
1267
+ popover.style.overflowY = "auto"
1268
+ popoverHeight = Math.min(availableHeight, popover.offsetHeight)
1269
+ }
1257
1270
 
1258
- const availableBelow = Math.max(0, window.innerHeight - viewportPadding - top)
1259
- if (availableBelow > 0 && popoverHeight > availableBelow) {
1260
- popover.style.maxHeight = `${Math.round(availableBelow)}px`
1261
- popover.style.overflowY = "auto"
1262
- popoverHeight = Math.min(availableBelow, popover.offsetHeight)
1271
+ if (top + popoverHeight > window.innerHeight - viewportPadding) {
1272
+ top = window.innerHeight - popoverHeight - viewportPadding
1273
+ }
1274
+ if (top < viewportPadding) {
1275
+ top = viewportPadding
1276
+ }
1277
+ } else {
1278
+ left = rect.left
1279
+ top = Math.max(viewportPadding, rect.bottom + dropOffset)
1280
+
1281
+ if (left + popoverWidth > window.innerWidth - viewportPadding) {
1282
+ left = window.innerWidth - popoverWidth - viewportPadding
1283
+ }
1284
+ if (left < viewportPadding) {
1285
+ left = viewportPadding
1286
+ }
1287
+
1288
+ const availableBelow = Math.max(0, window.innerHeight - viewportPadding - top)
1289
+ if (availableBelow > 0 && popoverHeight > availableBelow) {
1290
+ popover.style.maxHeight = `${Math.round(availableBelow)}px`
1291
+ popover.style.overflowY = "auto"
1292
+ popoverHeight = Math.min(availableBelow, popover.offsetHeight)
1293
+ }
1263
1294
  }
1264
1295
 
1265
1296
  popover.style.left = `${Math.round(left)}px`