pinokiod 3.153.0 → 3.156.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.153.0",
3
+ "version": "3.156.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -6742,7 +6742,6 @@ class Server {
6742
6742
  if (preferHttps) {
6743
6743
  httpsHosts = resolveHttpsHosts(item)
6744
6744
  }
6745
- console.log("httpsHosts", httpsHosts)
6746
6745
  const httpsUrls = httpsHosts.map((host) => {
6747
6746
  if (!host) {
6748
6747
  return null
@@ -6753,7 +6752,6 @@ class Server {
6753
6752
  }
6754
6753
  return `https://${trimmed}`
6755
6754
  }).filter(Boolean)
6756
- console.log("httpsUrls", httpsUrls)
6757
6755
 
6758
6756
  const preferredHttpsUrl = preferFriendlyHost(httpsHosts)
6759
6757
  const displayHttpsUrl = preferredHttpsUrl
@@ -6783,6 +6781,160 @@ class Server {
6783
6781
  })
6784
6782
 
6785
6783
  this.add_extra_urls(info)
6784
+
6785
+ const toArray = (value) => {
6786
+ if (!value) return []
6787
+ return Array.isArray(value) ? value.filter(Boolean) : [value].filter(Boolean)
6788
+ }
6789
+
6790
+ const uniqueUrls = (urls) => {
6791
+ const seen = new Set()
6792
+ const result = []
6793
+ for (const url of urls) {
6794
+ if (typeof url !== 'string') continue
6795
+ const trimmed = url.trim()
6796
+ if (!trimmed || seen.has(trimmed)) {
6797
+ continue
6798
+ }
6799
+ seen.add(trimmed)
6800
+ result.push(trimmed)
6801
+ }
6802
+ return result
6803
+ }
6804
+
6805
+ const isLoopbackHost = (url) => {
6806
+ try {
6807
+ const hostname = new URL(url).hostname.toLowerCase()
6808
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
6809
+ } catch (error) {
6810
+ return false
6811
+ }
6812
+ }
6813
+
6814
+ const stripPeerFromHostname = (url, peerName) => {
6815
+ if (!url || !peerName) {
6816
+ return url
6817
+ }
6818
+ try {
6819
+ const parsed = new URL(url)
6820
+ const suffix = `.${peerName}.localhost`
6821
+ if (parsed.hostname.toLowerCase().endsWith(suffix.toLowerCase())) {
6822
+ const prefix = parsed.hostname.slice(0, -suffix.length)
6823
+ if (!prefix) {
6824
+ return url
6825
+ }
6826
+ const sanitizedHostname = `${prefix}.localhost`
6827
+ const portPart = parsed.port ? `:${parsed.port}` : ''
6828
+ return `${parsed.protocol}//${sanitizedHostname}${portPart}${parsed.pathname}${parsed.search}${parsed.hash}`
6829
+ }
6830
+ return url
6831
+ } catch (error) {
6832
+ return url
6833
+ }
6834
+ }
6835
+
6836
+ const urlContainsPeerName = (url, peerName) => {
6837
+ if (!url || !peerName) {
6838
+ return false
6839
+ }
6840
+ try {
6841
+ const hostname = new URL(url).hostname.toLowerCase()
6842
+ const needle = `.${peerName.toLowerCase()}.`
6843
+ return hostname.includes(needle)
6844
+ } catch (error) {
6845
+ return false
6846
+ }
6847
+ }
6848
+
6849
+ const selectPrimaryUrl = (entry) => {
6850
+ const hostMeta = entry.host || {}
6851
+ const isLocalHost = hostMeta.local === true
6852
+ const peerName = typeof hostMeta.name === 'string' ? hostMeta.name : ''
6853
+
6854
+ const httpCandidates = toArray(entry.urls && entry.urls.http)
6855
+ const httpsCandidates = toArray(entry.urls && entry.urls.https)
6856
+
6857
+ let sanitizedHttpsCandidates = httpsCandidates.slice()
6858
+ if (preferHttps && isLocalHost) {
6859
+ sanitizedHttpsCandidates = uniqueUrls(sanitizedHttpsCandidates.map((url) => {
6860
+ return stripPeerFromHostname(url, peerName)
6861
+ }))
6862
+ }
6863
+
6864
+ let primaryUrl
6865
+
6866
+ if (preferHttps) {
6867
+ let candidates = sanitizedHttpsCandidates.slice()
6868
+
6869
+ if (!isLocalHost && peerName) {
6870
+ const withPeer = candidates.filter((url) => urlContainsPeerName(url, peerName))
6871
+ if (withPeer.length > 0) {
6872
+ candidates = withPeer
6873
+ }
6874
+ }
6875
+
6876
+ primaryUrl = candidates[0]
6877
+
6878
+ if (!primaryUrl && httpCandidates.length > 0) {
6879
+ primaryUrl = httpCandidates[0]
6880
+ }
6881
+ } else {
6882
+ let candidates = httpCandidates.slice()
6883
+
6884
+ if (isLocalHost) {
6885
+ const loopbackCandidate = candidates.find((url) => isLoopbackHost(url))
6886
+ if (loopbackCandidate) {
6887
+ candidates = [loopbackCandidate]
6888
+ }
6889
+ } else {
6890
+ const nonLoopback = candidates.filter((url) => !isLoopbackHost(url))
6891
+ if (nonLoopback.length > 0) {
6892
+ candidates = nonLoopback
6893
+ }
6894
+ }
6895
+
6896
+ primaryUrl = candidates[0]
6897
+
6898
+ if (!primaryUrl) {
6899
+ let httpsFallback = sanitizedHttpsCandidates.slice()
6900
+
6901
+ if (isLocalHost) {
6902
+ httpsFallback = uniqueUrls(httpsFallback.map((url) => stripPeerFromHostname(url, peerName)))
6903
+ } else if (peerName) {
6904
+ const withPeer = httpsFallback.filter((url) => urlContainsPeerName(url, peerName))
6905
+ if (withPeer.length > 0) {
6906
+ httpsFallback = withPeer
6907
+ }
6908
+ }
6909
+
6910
+ primaryUrl = httpsFallback[0]
6911
+ }
6912
+ }
6913
+
6914
+ if (!primaryUrl) {
6915
+ primaryUrl = entry.url || httpCandidates[0] || sanitizedHttpsCandidates[0] || httpsCandidates[0] || null
6916
+ }
6917
+
6918
+ if (primaryUrl) {
6919
+ entry.url = primaryUrl
6920
+ entry.protocol = primaryUrl.startsWith('https://') ? 'https' : 'http'
6921
+
6922
+ entry.urls = {
6923
+ http: primaryUrl.startsWith('http://') ? [primaryUrl] : [],
6924
+ https: primaryUrl.startsWith('https://') ? [primaryUrl] : []
6925
+ }
6926
+ } else {
6927
+ entry.urls = {
6928
+ http: [],
6929
+ https: []
6930
+ }
6931
+ }
6932
+ }
6933
+
6934
+ for (const entry of info) {
6935
+ selectPrimaryUrl(entry)
6936
+ }
6937
+
6786
6938
  res.json({
6787
6939
  info
6788
6940
  })
@@ -812,6 +812,7 @@ form.search .btn.clone {
812
812
  }
813
813
 
814
814
  body {
815
+ background: white;
815
816
  height: 100%;
816
817
  box-sizing: border-box;
817
818
  margin: 0;
@@ -2449,7 +2450,8 @@ body.dark .mode-selector .btn2.selected {
2449
2450
  }
2450
2451
  main,
2451
2452
  iframe.mainframe {
2452
- padding-left: 55px;
2453
+ margin-left: 55px;
2454
+ width: calc(100% - 55px);
2453
2455
  }
2454
2456
  main .container {
2455
2457
  margin-left: 10px !important;
@@ -93,68 +93,34 @@ body.dark .url-dropdown-name {
93
93
  .url-dropdown-url {
94
94
  color: #718096;
95
95
  font-size: 12px;
96
- }
97
- body.dark .url-dropdown-url {
98
- color: #a0aec0;
99
- }
100
- .url-dropdown-url-list {
101
96
  display: flex;
102
- flex-wrap: wrap;
103
- gap: 6px;
104
- }
105
- .url-chip {
106
- display: inline-flex;
107
97
  align-items: center;
108
- gap: 6px;
109
- padding: 4px 8px;
110
- border-radius: 6px;
111
- border: 1px solid #e2e8f0;
112
- background: #f8fafc;
113
- color: #2d3748;
114
- font-size: 12px;
115
- font-weight: 500;
116
- cursor: pointer;
117
- transition: background 0.15s ease, border-color 0.15s ease;
118
- }
119
- .url-chip:hover {
120
- background: #edf2f7;
121
- border-color: #cbd5f5;
122
- }
123
- body.dark .url-chip {
124
- border-color: rgba(255,255,255,0.15);
125
- background: rgba(255,255,255,0.06);
126
- color: #e2e8f0;
98
+ gap: 8px;
127
99
  }
128
- body.dark .url-chip:hover {
129
- background: rgba(255,255,255,0.1);
130
- border-color: rgba(255,255,255,0.22);
100
+ body.dark .url-dropdown-url {
101
+ color: #a0aec0;
131
102
  }
132
- .url-chip .chip-mark {
103
+ .url-scheme {
133
104
  font-size: 10px;
134
105
  font-weight: 600;
135
106
  letter-spacing: 0.05em;
136
107
  text-transform: uppercase;
137
- padding: 2px 4px;
108
+ padding: 2px 6px;
138
109
  border-radius: 4px;
110
+ background: rgba(237, 137, 54, 0.18);
111
+ color: #9c4221;
139
112
  }
140
- .url-chip-https .chip-mark {
113
+ .url-scheme.https {
141
114
  background: rgba(72, 187, 120, 0.18);
142
115
  color: #276749;
143
116
  }
144
- body.dark .url-chip-https .chip-mark {
145
- background: rgba(72, 187, 120, 0.25);
146
- color: #9ae6b4;
147
- }
148
- .url-chip-http .chip-mark {
149
- background: rgba(237, 137, 54, 0.18);
150
- color: #9c4221;
151
- }
152
- body.dark .url-chip-http .chip-mark {
117
+ body.dark .url-scheme {
153
118
  background: rgba(237, 137, 54, 0.25);
154
119
  color: #fbd38d;
155
120
  }
156
- .url-chip .chip-label {
157
- font-weight: 500;
121
+ body.dark .url-scheme.https {
122
+ background: rgba(72, 187, 120, 0.25);
123
+ color: #9ae6b4;
158
124
  }
159
125
  .url-dropdown-loading {
160
126
  padding: 12px 16px;
@@ -82,8 +82,9 @@ function initUrlDropdown(config = {}) {
82
82
 
83
83
  const getProcessUrls = (process) => {
84
84
  const urls = (process && process.urls) || {};
85
- const httpUrl = urls.http || (process && process.ip ? `http://${process.ip}` : null);
86
- const httpsUrls = toArray(urls.https || (process && process.protocol === 'https' && process.url ? process.url : null))
85
+ const httpCandidates = toArray(urls.http);
86
+ const httpsCandidates = toArray(urls.https);
87
+ const normalizedHttps = httpsCandidates
87
88
  .map((value) => {
88
89
  if (typeof value !== 'string') return null;
89
90
  const trimmed = value.trim();
@@ -94,12 +95,19 @@ function initUrlDropdown(config = {}) {
94
95
  return `https://${trimmed}`;
95
96
  })
96
97
  .filter(Boolean);
97
- return { httpUrl, httpsUrls };
98
+ const httpUrl = httpCandidates.length > 0
99
+ ? httpCandidates[0]
100
+ : (process && process.ip ? `http://${process.ip}` : null);
101
+ return {
102
+ httpUrl,
103
+ httpUrls: httpCandidates.filter((value) => typeof value === 'string' && value.trim().length > 0),
104
+ httpsUrls: normalizedHttps
105
+ };
98
106
  };
99
107
 
100
108
  const getProcessDisplayUrl = (process) => {
101
109
  if (process && typeof process.url === 'string' && process.url.trim().length > 0) {
102
- return process.url;
110
+ return process.url.trim();
103
111
  }
104
112
  const { httpUrl, httpsUrls } = getProcessUrls(process);
105
113
  if (httpsUrls.length > 0) {
@@ -114,11 +122,9 @@ function initUrlDropdown(config = {}) {
114
122
  if (display) {
115
123
  urls.add(display);
116
124
  }
117
- const { httpUrl, httpsUrls } = getProcessUrls(process);
118
- if (httpUrl) {
119
- urls.add(httpUrl);
120
- }
121
- httpsUrls.forEach((httpsUrl) => urls.add(httpsUrl));
125
+ const { httpUrls, httpsUrls } = getProcessUrls(process);
126
+ httpUrls.forEach((value) => urls.add(value));
127
+ httpsUrls.forEach((value) => urls.add(value));
122
128
  return Array.from(urls);
123
129
  };
124
130
 
@@ -163,43 +169,6 @@ function initUrlDropdown(config = {}) {
163
169
  return url.replace(/^https?:\/\//i, '');
164
170
  };
165
171
 
166
- const createUrlChip = (url, type, scheme) => {
167
- if (!url) {
168
- return '';
169
- }
170
- const normalizedType = type || 'remote';
171
- const classSuffix = scheme === 'HTTPS' ? 'https' : 'http';
172
- const label = formatUrlLabel(url);
173
- return `
174
- <button type="button" class="url-chip url-chip-${classSuffix}" data-url="${escapeAttribute(url)}" data-host-type="${escapeAttribute(normalizedType)}">
175
- <span class="chip-mark">${scheme}</span>
176
- <span class="chip-label">${escapeHtml(label)}</span>
177
- </button>
178
- `;
179
- };
180
-
181
- const buildProcessUrlList = (process) => {
182
- const { httpUrl, httpsUrls } = getProcessUrls(process);
183
- const hostType = (process && process.host && process.host.local) ? 'local' : 'remote';
184
- const parts = [];
185
- const seen = new Set();
186
- httpsUrls.forEach((candidate) => {
187
- if (!candidate || seen.has(candidate)) {
188
- return;
189
- }
190
- parts.push(createUrlChip(candidate, hostType, 'HTTPS'));
191
- seen.add(candidate);
192
- });
193
- if (httpUrl && !seen.has(httpUrl)) {
194
- parts.push(createUrlChip(httpUrl, hostType, 'HTTP'));
195
- seen.add(httpUrl);
196
- }
197
- if (parts.length === 0) {
198
- return '';
199
- }
200
- return `<div class="url-dropdown-url-list">${parts.join('')}</div>`;
201
- };
202
-
203
172
  let isDropdownVisible = false;
204
173
  let allProcesses = []; // Store all processes for filtering
205
174
  let filteredProcesses = []; // Store currently filtered processes
@@ -449,89 +418,103 @@ function initUrlDropdown(config = {}) {
449
418
  `;
450
419
  }
451
420
 
452
- function populateDropdown(processes) {
453
- const currentUrl = window.location.href;
454
- const currentTitle = document.title || 'Current tab';
421
+ const buildProcessItemHtml = (process) => {
422
+ const onlineIndicator = process.online ?
423
+ '<div class="status-circle online"></div>' :
424
+ '<div class="status-circle offline"></div>';
455
425
 
456
- let html = '';
457
- if (currentUrl) {
458
- html += `
459
- <div class="url-dropdown-host-header current-tab">
460
- <span class="host-name">Current tab</span>
461
- </div>
462
- <div class="url-dropdown-item" data-url="${escapeAttribute(currentUrl)}" data-host-type="current">
426
+ if (process.ip === null || process.ip === undefined) {
427
+ const networkUrl = `http://${process.host.ip}:42000/network`;
428
+ return `
429
+ <div class="url-dropdown-item non-selectable">
463
430
  <div class="url-dropdown-name">
464
- <span>
465
- <i class="fa-solid fa-clone"></i>
466
- ${escapeHtml(currentTitle)}
467
- </span>
431
+ ${onlineIndicator}
432
+ <button class="peer-network-button" data-network-url="${escapeAttribute(networkUrl)}"><i class="fa-solid fa-toggle-on"></i> Turn on peer network</button>
433
+ ${escapeHtml(process.name)}
468
434
  </div>
469
- <div class="url-dropdown-url">${escapeHtml(currentUrl)}</div>
470
435
  </div>
471
436
  `;
472
437
  }
473
438
 
474
- if (processes.length === 0) {
475
- html += createEmptyStateHtml(getEmptyStateMessage(urlInput));
476
- dropdown.innerHTML = html;
477
- attachCreateButtonHandler(dropdown, urlInput);
478
- attachUrlItemHandlers(dropdown);
479
- return;
439
+ const displayUrl = getProcessDisplayUrl(process);
440
+ if (!displayUrl) {
441
+ return '';
480
442
  }
443
+ const selectionType = (process.host && process.host.local) ? 'local' : 'remote';
444
+ const schemeLabel = displayUrl.startsWith('https://') ? 'HTTPS' : 'HTTP';
445
+ const formattedUrl = formatUrlLabel(displayUrl);
446
+ return `
447
+ <div class="url-dropdown-item" data-url="${escapeAttribute(displayUrl)}" data-host-type="${escapeAttribute(selectionType)}">
448
+ <div class="url-dropdown-name">
449
+ ${onlineIndicator}
450
+ ${escapeHtml(process.name)}
451
+ </div>
452
+ <div class="url-dropdown-url">
453
+ <span class="url-scheme ${schemeLabel === 'HTTPS' ? 'https' : 'http'}">${schemeLabel}</span>
454
+ <span class="url-address">${escapeHtml(formattedUrl || displayUrl)}</span>
455
+ </div>
456
+ </div>
457
+ `;
458
+ };
481
459
 
482
- // Group processes by host
460
+ const buildHostsHtml = (processes) => {
483
461
  const groupedProcesses = groupProcessesByHost(processes);
462
+ let html = '';
484
463
 
485
464
  Object.keys(groupedProcesses).forEach(hostKey => {
486
465
  const hostData = groupedProcesses[hostKey];
487
466
  const hostInfo = hostData.host;
488
467
  const hostProcesses = hostData.processes;
489
468
  const isLocal = hostData.isLocal;
490
-
491
- // Add host header
469
+
492
470
  html += createHostHeader(hostInfo, isLocal);
493
-
494
- // Add processes for this host
495
- hostProcesses.forEach(process => {
496
- const onlineIndicator = process.online ?
497
- '<div class="status-circle online"></div>' :
498
- '<div class="status-circle offline"></div>';
499
-
500
- if (process.ip === null || process.ip === undefined) {
501
- // Non-selectable item with "turn on peer network" button
502
- const networkUrl = `http://${process.host.ip}:42000/network`;
503
- html += `
504
- <div class="url-dropdown-item non-selectable">
505
- <div class="url-dropdown-name">
506
- ${onlineIndicator}
507
- <button class="peer-network-button" data-network-url="${escapeAttribute(networkUrl)}"><i class="fa-solid fa-toggle-on"></i> Turn on peer network</button>
508
- ${escapeHtml(process.name)}
509
- </div>
510
- </div>
511
- `;
512
- return;
513
- }
514
471
 
515
- const displayUrl = getProcessDisplayUrl(process);
516
- if (!displayUrl) {
517
- return;
518
- }
519
- const selectionType = (process.host && process.host.local) ? 'local' : 'remote';
520
- const urlListHtml = buildProcessUrlList(process);
521
- html += `
522
- <div class="url-dropdown-item" data-url="${escapeAttribute(displayUrl)}" data-host-type="${escapeAttribute(selectionType)}">
523
- <div class="url-dropdown-name">
524
- ${onlineIndicator}
525
- ${escapeHtml(process.name)}
526
- </div>
527
- <div class="url-dropdown-url">${escapeHtml(displayUrl)}</div>
528
- ${urlListHtml}
529
- </div>
530
- `;
472
+ hostProcesses.forEach(process => {
473
+ html += buildProcessItemHtml(process);
531
474
  });
532
475
  });
533
476
 
534
- dropdown.innerHTML = html;
477
+ return html;
478
+ };
479
+
480
+ const buildDropdownHtml = (processes, { includeCurrentTab = true, inputElement } = {}) => {
481
+ const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
482
+ const currentTitle = typeof document !== 'undefined' ? (document.title || 'Current tab') : 'Current tab';
483
+
484
+ let html = '';
485
+ if (includeCurrentTab && currentUrl) {
486
+ const schemeLabel = currentUrl.startsWith('https://') ? 'HTTPS' : 'HTTP';
487
+ html += `
488
+ <div class="url-dropdown-host-header current-tab">
489
+ <span class="host-name">Current tab</span>
490
+ </div>
491
+ <div class="url-dropdown-item" data-url="${escapeAttribute(currentUrl)}" data-host-type="current">
492
+ <div class="url-dropdown-name">
493
+ <span>
494
+ <i class="fa-solid fa-clone"></i>
495
+ ${escapeHtml(currentTitle)}
496
+ </span>
497
+ </div>
498
+ <div class="url-dropdown-url">
499
+ <span class="url-scheme ${schemeLabel === 'HTTPS' ? 'https' : 'http'}">${schemeLabel}</span>
500
+ <span class="url-address">${escapeHtml(formatUrlLabel(currentUrl) || currentUrl)}</span>
501
+ </div>
502
+ </div>
503
+ `;
504
+ }
505
+
506
+ if (!processes || processes.length === 0) {
507
+ html += createEmptyStateHtml(getEmptyStateMessage(inputElement));
508
+ return html;
509
+ }
510
+
511
+ html += buildHostsHtml(processes);
512
+ return html;
513
+ };
514
+
515
+ function populateDropdown(processes) {
516
+ dropdown.innerHTML = buildDropdownHtml(processes, { includeCurrentTab: true, inputElement: urlInput });
517
+ attachCreateButtonHandler(dropdown, urlInput);
535
518
  attachUrlItemHandlers(dropdown);
536
519
  }
537
520
 
@@ -548,21 +531,19 @@ function initUrlDropdown(config = {}) {
548
531
  openUrlWithType(url, type);
549
532
  }
550
533
 
551
- function attachUrlItemHandlers(container) {
534
+ function attachUrlItemHandlers(container, options = {}) {
552
535
  if (!container) return;
536
+ const onSelect = typeof options.onSelect === 'function' ? options.onSelect : handleSelection;
537
+ const selectionOptions = options.selectionOptions || {};
553
538
  container.querySelectorAll('.url-dropdown-item:not(.non-selectable)').forEach(item => {
554
539
  item.addEventListener('click', function() {
555
540
  const url = this.getAttribute('data-url');
556
541
  const type = this.getAttribute('data-host-type');
557
- handleSelection(url, type);
558
- });
559
- });
560
- container.querySelectorAll('.url-chip').forEach(button => {
561
- button.addEventListener('click', function(event) {
562
- event.stopPropagation();
563
- const url = this.getAttribute('data-url');
564
- const type = this.getAttribute('data-host-type');
565
- handleSelection(url, type);
542
+ if (onSelect === handleSelection) {
543
+ onSelect(url, type, selectionOptions);
544
+ } else {
545
+ onSelect(url, type);
546
+ }
566
547
  });
567
548
  });
568
549
  container.querySelectorAll('.peer-network-button').forEach(button => {
@@ -829,6 +810,9 @@ function initUrlDropdown(config = {}) {
829
810
  refs.context = customOptions.context || 'default';
830
811
  refs.returnSelection = Boolean(customOptions.awaitSelection);
831
812
  refs.resolve = null;
813
+ if (refs.dropdown) {
814
+ refs.dropdown._includeCurrent = includeCurrent;
815
+ }
832
816
 
833
817
  modalInput.value = initialValue;
834
818
  updateConfirmState();
@@ -923,15 +907,28 @@ function initUrlDropdown(config = {}) {
923
907
  }
924
908
 
925
909
  function showModalDropdown(modalDropdown) {
910
+ if (!modalDropdown || !modalDropdown.parentElement) return;
926
911
  modalDropdown.style.display = 'block';
927
-
912
+
913
+ const includeCurrent = modalDropdown._includeCurrent !== false;
914
+ modalDropdown._includeCurrent = includeCurrent;
915
+
916
+ const render = (processes) => {
917
+ modalDropdown.innerHTML = buildDropdownHtml(processes, {
918
+ includeCurrentTab: includeCurrent,
919
+ inputElement: modalDropdown.parentElement.querySelector('.url-modal-input')
920
+ });
921
+ attachCreateButtonHandler(modalDropdown, modalDropdown.parentElement.querySelector('.url-modal-input'));
922
+ attachUrlItemHandlers(modalDropdown, { onSelect: handleModalSelection });
923
+ };
924
+
928
925
  if (allProcesses.length > 0) {
929
- populateModalDropdown(allProcesses, modalDropdown);
926
+ render(allProcesses);
930
927
  return;
931
928
  }
932
-
929
+
933
930
  modalDropdown.innerHTML = '<div class="url-dropdown-loading">Loading running processes...</div>';
934
-
931
+
935
932
  fetch(options.apiEndpoint)
936
933
  .then(response => {
937
934
  if (!response.ok) {
@@ -941,7 +938,7 @@ function initUrlDropdown(config = {}) {
941
938
  })
942
939
  .then(data => {
943
940
  allProcesses = data.info || [];
944
- populateModalDropdown(allProcesses, modalDropdown);
941
+ render(allProcesses);
945
942
  })
946
943
  .catch(error => {
947
944
  console.error('Failed to fetch processes:', error);
@@ -971,103 +968,16 @@ function initUrlDropdown(config = {}) {
971
968
 
972
969
  function populateModalDropdown(processes, modalDropdown) {
973
970
  const modalInput = modalDropdown.parentElement.querySelector('.url-modal-input');
974
- const currentUrl = window.location.href;
975
- const currentTitle = document.title || 'Current tab';
976
971
  const overlayRefs = getModalRefs();
977
972
  const includeCurrent = overlayRefs?.includeCurrent !== false;
978
973
 
979
- let html = '';
980
-
981
- if (includeCurrent && currentUrl) {
982
- html += `
983
- <div class="url-dropdown-host-header current-tab">
984
- <span class="host-name">Current tab</span>
985
- </div>
986
- <div class="url-dropdown-item" data-url="${escapeHtml(currentUrl)}" data-host-type="current">
987
- <div class="url-dropdown-name">
988
- <i class="fa-solid fa-clone"></i>
989
- <span>${escapeHtml(currentTitle)}</span>
990
- </div>
991
- <div class="url-dropdown-url">${escapeHtml(currentUrl)}</div>
992
- </div>
993
- `;
994
- }
995
-
996
- if (processes.length === 0) {
997
- html += createEmptyStateHtml(getEmptyStateMessage(modalInput));
998
- modalDropdown.innerHTML = html;
999
- attachCreateButtonHandler(modalDropdown, modalInput);
1000
-
1001
- modalDropdown.querySelectorAll('.url-dropdown-item:not(.non-selectable)').forEach(item => {
1002
- item.addEventListener('click', function() {
1003
- const url = this.getAttribute('data-url');
1004
- const type = this.getAttribute('data-host-type');
1005
- handleModalSelection(url, type);
1006
- });
1007
- });
1008
- return;
1009
- }
1010
-
1011
- const groupedProcesses = groupProcessesByHost(processes);
1012
- Object.keys(groupedProcesses).forEach(hostKey => {
1013
- const hostData = groupedProcesses[hostKey];
1014
- const hostInfo = hostData.host;
1015
- const hostProcesses = hostData.processes;
1016
- const isLocal = hostData.isLocal;
1017
-
1018
- html += createHostHeader(hostInfo, isLocal);
1019
-
1020
- hostProcesses.forEach(process => {
1021
- const onlineIndicator = process.online ?
1022
- '<div class="status-circle online"></div>' :
1023
- '<div class="status-circle offline"></div>';
1024
-
1025
- if (process.ip === null || process.ip === undefined) {
1026
- const networkUrl = `http://${process.host.ip}:42000/network`;
1027
- html += `
1028
- <div class="url-dropdown-item non-selectable">
1029
- <div class="url-dropdown-name">
1030
- ${onlineIndicator}
1031
- ${escapeHtml(process.name)}
1032
- </div>
1033
- <button class="peer-network-button" data-network-url="${networkUrl}"><i class="fa-solid fa-toggle-on"></i> Turn on peer network</button>
1034
- </div>
1035
- `;
1036
- } else {
1037
- const url = getProcessDisplayUrl(process);
1038
- if (!url) {
1039
- return;
1040
- }
1041
- html += `
1042
- <div class="url-dropdown-item" data-url="${url}" data-host-type="${process.host.local ? "local" : "remote"}">
1043
- <div class="url-dropdown-name">
1044
- ${onlineIndicator}
1045
- ${escapeHtml(process.name)}
1046
- </div>
1047
- <div class="url-dropdown-url">${escapeHtml(url)}</div>
1048
- </div>
1049
- `;
1050
- }
1051
- });
1052
- });
1053
-
1054
- modalDropdown.innerHTML = html;
1055
-
1056
- modalDropdown.querySelectorAll('.url-dropdown-item:not(.non-selectable)').forEach(item => {
1057
- item.addEventListener('click', function() {
1058
- const url = this.getAttribute('data-url');
1059
- const type = this.getAttribute('data-host-type');
1060
- handleModalSelection(url, type);
1061
- });
974
+ modalDropdown.innerHTML = buildDropdownHtml(processes, {
975
+ includeCurrentTab: includeCurrent,
976
+ inputElement: modalInput
1062
977
  });
1063
978
 
1064
- modalDropdown.querySelectorAll('.peer-network-button').forEach(button => {
1065
- button.addEventListener('click', function(e) {
1066
- e.stopPropagation();
1067
- const networkUrl = this.getAttribute('data-network-url');
1068
- window.open(networkUrl, '_blank', 'self');
1069
- });
1070
- });
979
+ attachCreateButtonHandler(modalDropdown, modalInput);
980
+ attachUrlItemHandlers(modalDropdown, { onSelect: handleModalSelection });
1071
981
  }
1072
982
 
1073
983
  // Set up mobile button click handler
@@ -62,6 +62,7 @@
62
62
  }
63
63
 
64
64
  .layout-leaf {
65
+ background: var(--layout-background);
65
66
  position: absolute;
66
67
  box-sizing: border-box;
67
68
  /*