pinokiod 3.97.0 → 3.98.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.97.0",
3
+ "version": "3.98.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -164,7 +164,6 @@ aside .tab.submenu {
164
164
  #genlog, #downloadlogs {
165
165
  cursor: pointer;
166
166
  }
167
-
168
167
  .url-bar {
169
168
  font-family: verdana;
170
169
  font-size: 12px;
@@ -295,7 +294,7 @@ body.dark .navheader2 .btn {
295
294
  /*
296
295
  background: white;
297
296
  */
298
- background: rgba(0,0,0,0.06) !important;
297
+ background: #F9F9FB !important;
299
298
  }
300
299
  .navheader {
301
300
  backdrop-filter: blur(16px);
@@ -18,6 +18,9 @@
18
18
  <link href="/electron.css" rel="stylesheet"/>
19
19
  <% } %>
20
20
  <style>
21
+ body.dark #devtab {
22
+ border: none;
23
+ }
21
24
  #devtab {
22
25
  align-items: center;
23
26
  justify-content: center;
@@ -31,6 +34,7 @@
31
34
  background: none;
32
35
  border-bottom-left-radius: 0;
33
36
  border-bottom-right-radius: 0;
37
+ border-bottom: none;
34
38
  }
35
39
  #devtab .loader {
36
40
  padding: 10px;
@@ -141,10 +145,12 @@ body.dark .appcanvas {
141
145
  */
142
146
  body.dark .appcanvas_filler {
143
147
  background: rgba(255,255,255,0.2) !important;
148
+ border: none;
144
149
  }
145
150
  .appcanvas_filler {
146
151
  height: 8px;
147
- background: rgba(0,0,0,0.06) !important;
152
+ background: #F9F9FB !important;
153
+ border-top: 1px solid var(--pinokio-sidebar-tabbar-border);
148
154
  }
149
155
 
150
156
  .appcanvas > .container {
@@ -379,13 +385,17 @@ body.dark .appcanvas > aside .m.menu .nested-menu > .submenu .frame-link:hover {
379
385
  color: var(--pinokio-sidebar-tab-active-color);
380
386
  }
381
387
 
388
+ body.dark .appcanvas > aside .header-item.selected {
389
+ border: none;
390
+ }
382
391
  .appcanvas > aside .header-item.selected {
383
- background: var(--pinokio-sidebar-tab-active-bg);
392
+ /*
393
+ border-color: var(--pinokio-sidebar-tab-active-bg);
394
+ */
384
395
  color: var(--pinokio-sidebar-tab-active-color);
385
- /*
386
396
  border-color: var(--sidebar-tab-outline);
387
- */
388
397
  box-shadow: 0 4px 12px var(--pinokio-sidebar-tab-shadow);
398
+ background: #F9F9FB !important;
389
399
  border-bottom: none;
390
400
  z-index: 1;
391
401
  }
@@ -685,11 +695,8 @@ body.dark .header-item.cursor {
685
695
  cursor: pointer;
686
696
  }
687
697
  body .frame-link.selected {
688
- background: rgba(0,0,0,0.06) !important;
689
- /*
690
- background: rgba(127, 91, 243, 0.9) !important;
691
- color: white !important;
692
- */
698
+ background: #F9F9FB !important;
699
+ border: 1px solid rgba(203, 213, 225, 0.7);
693
700
  }
694
701
  .frame-link.selected .del {
695
702
  color: white;
@@ -791,6 +798,94 @@ body.dark .grid-btns .btn2 {
791
798
  flex: 1 1 auto;
792
799
  min-width: 0;
793
800
  }
801
+ .tab-link-popover {
802
+ position: fixed;
803
+ display: none;
804
+ flex-direction: column;
805
+ gap: 4px;
806
+ padding: 8px 0;
807
+ border-radius: 10px;
808
+ border: 1px solid rgba(0, 0, 0, 0.12);
809
+ background: rgba(255, 255, 255, 0.97);
810
+ box-shadow: 0 12px 32px -8px rgba(15, 23, 42, 0.25);
811
+ z-index: 9999;
812
+ min-width: 240px;
813
+ max-width: 420px;
814
+ backdrop-filter: blur(6px);
815
+ }
816
+ body.dark .tab-link-popover {
817
+ border-color: rgba(255, 255, 255, 0.08);
818
+ background: rgba(17, 24, 39, 0.95);
819
+ box-shadow: 0 12px 36px -12px rgba(15, 23, 42, 0.55);
820
+ }
821
+ .tab-link-popover.visible {
822
+ display: flex;
823
+ }
824
+ .tab-link-popover .tab-link-popover-header {
825
+ display: flex;
826
+ align-items: center;
827
+ gap: 8px;
828
+ padding: 10px 14px 6px;
829
+ font-size: 11px;
830
+ font-weight: 600;
831
+ letter-spacing: 0.05em;
832
+ text-transform: uppercase;
833
+ color: rgba(15, 23, 42, 0.55);
834
+ }
835
+ .tab-link-popover .tab-link-popover-header i {
836
+ font-size: 12px;
837
+ }
838
+ body.dark .tab-link-popover .tab-link-popover-header {
839
+ color: rgba(226, 232, 240, 0.7);
840
+ }
841
+ .tab-link-popover .tab-link-popover-item {
842
+ width: 100%;
843
+ border: none;
844
+ margin: 0;
845
+ padding: 8px 14px;
846
+ display: flex;
847
+ flex-direction: column;
848
+ gap: 2px;
849
+ text-align: left;
850
+ font: inherit;
851
+ color: inherit;
852
+ background: transparent;
853
+ cursor: pointer;
854
+ }
855
+ .tab-link-popover .tab-link-popover-item:hover,
856
+ .tab-link-popover .tab-link-popover-item:focus-visible {
857
+ background: rgba(15, 23, 42, 0.06);
858
+ outline: none;
859
+ }
860
+ body.dark .tab-link-popover .tab-link-popover-item:hover,
861
+ body.dark .tab-link-popover .tab-link-popover-item:focus-visible {
862
+ background: rgba(148, 163, 184, 0.12);
863
+ }
864
+ .tab-link-popover .tab-link-popover-item .label {
865
+ font-size: 11px;
866
+ font-weight: 600;
867
+ letter-spacing: 0.04em;
868
+ text-transform: uppercase;
869
+ color: rgba(15, 23, 42, 0.55);
870
+ }
871
+ body.dark .tab-link-popover .tab-link-popover-item .label {
872
+ color: rgba(226, 232, 240, 0.65);
873
+ }
874
+ .tab-link-popover .tab-link-popover-item .value {
875
+ font-size: 13px;
876
+ line-height: 1.35;
877
+ word-break: break-word;
878
+ color: rgba(15, 23, 42, 0.85);
879
+ }
880
+ body.dark .tab-link-popover .tab-link-popover-item .value {
881
+ color: rgba(226, 232, 240, 0.9);
882
+ }
883
+ .tab-link-popover .tab-link-popover-item .value .muted {
884
+ color: rgba(15, 23, 42, 0.55);
885
+ }
886
+ body.dark .tab-link-popover .tab-link-popover-item .value .muted {
887
+ color: rgba(226, 232, 240, 0.65);
888
+ }
794
889
  .tab.has-preview .tab-preview {
795
890
  color: rgba(0, 0, 0, 0.6);
796
891
  min-width: 0;
@@ -1181,11 +1276,13 @@ body.dark #fs-status {
1181
1276
  /*
1182
1277
  border-bottom: 1px solid rgba(255,255,255,0.04);
1183
1278
  */
1279
+ border: none;
1184
1280
  background: rgba(255,255,255,0.2) !important;
1185
1281
  }
1186
1282
  #fs-status {
1187
1283
  padding: 5px;
1188
- background: rgba(0,0,0,0.06);
1284
+ background: #F9F9FB !important;
1285
+ border-top: 1px solid rgba(203, 213, 225, 0.7);
1189
1286
  gap: 4px;
1190
1287
  box-sizing: border-box;
1191
1288
  /*
@@ -2413,7 +2510,6 @@ body.dark {
2413
2510
  <a class="btn2 <%=type === 'browse' ? 'selected' : ''%>" href="<%=dev_tab%>"><div><i class="fa-solid fa-code"></i></div><div class='caption'>Dev</div></a>
2414
2511
  <a class="btn2 <%=type === 'run' ? 'selected' : ''%>" href="<%=run_tab%>"><div><i class="fa-solid fa-circle-play"></i></div><div class='caption'>Run</div></a>
2415
2512
  </div>
2416
- <button class='btn2' id='popout-link' data-tippy-content="open in a web browser"><i class="fa-solid fa-arrow-up-right-from-square"></i></button>
2417
2513
  <a class='btn2' href="/columns" data-tippy-content="split into 2 columns">
2418
2514
  <div><i class="fa-solid fa-table-columns"></i></div>
2419
2515
  </a>
@@ -2722,6 +2818,845 @@ body.dark {
2722
2818
  const updatedClass = "tab-updated"
2723
2819
  const tabStateStore = new Map()
2724
2820
  const TAB_STATE_STORAGE_KEY = "tab-state-store"
2821
+ const TAB_LINK_POPOVER_ID = "tab-link-popover"
2822
+ let tabLinkPopoverEl = null
2823
+ let tabLinkActiveLink = null
2824
+ let tabLinkPendingLink = null
2825
+ let tabLinkHideTimer = null
2826
+ let tabLinkLocalInfoPromise = null
2827
+ let tabLinkLocalInfoExpiry = 0
2828
+ let tabLinkRouterInfoPromise = null
2829
+ let tabLinkRouterInfoExpiry = 0
2830
+
2831
+ const ensureTabLinkPopoverEl = () => {
2832
+ if (!tabLinkPopoverEl) {
2833
+ tabLinkPopoverEl = document.createElement("div")
2834
+ tabLinkPopoverEl.id = TAB_LINK_POPOVER_ID
2835
+ tabLinkPopoverEl.className = "tab-link-popover"
2836
+ tabLinkPopoverEl.addEventListener("mouseenter", () => {
2837
+ if (tabLinkHideTimer) {
2838
+ clearTimeout(tabLinkHideTimer)
2839
+ tabLinkHideTimer = null
2840
+ }
2841
+ })
2842
+ tabLinkPopoverEl.addEventListener("mouseleave", () => {
2843
+ hideTabLinkPopover({ immediate: true })
2844
+ })
2845
+ tabLinkPopoverEl.addEventListener("click", (event) => {
2846
+ const item = event.target.closest(".tab-link-popover-item")
2847
+ if (!item) {
2848
+ return
2849
+ }
2850
+ event.preventDefault()
2851
+ event.stopPropagation()
2852
+ const url = item.getAttribute("data-url")
2853
+ if (url) {
2854
+ window.open(url, "_blank", "noopener")
2855
+ }
2856
+ hideTabLinkPopover({ immediate: true })
2857
+ })
2858
+ document.body.appendChild(tabLinkPopoverEl)
2859
+ }
2860
+ return tabLinkPopoverEl
2861
+ }
2862
+
2863
+ const canonicalizeUrl = (value) => {
2864
+ try {
2865
+ const parsed = new URL(value, location.origin)
2866
+ if (!parsed.protocol) {
2867
+ return value
2868
+ }
2869
+ const protocol = parsed.protocol.toLowerCase()
2870
+ if (protocol !== "http:" && protocol !== "https:") {
2871
+ return value
2872
+ }
2873
+ const hostname = parsed.hostname.toLowerCase()
2874
+ const port = parsed.port ? `:${parsed.port}` : ""
2875
+ let pathname = parsed.pathname || "/"
2876
+ if (pathname !== "/") {
2877
+ pathname = pathname.replace(/\/+/g, "/")
2878
+ if (pathname.length > 1 && pathname.endsWith("/")) {
2879
+ pathname = pathname.slice(0, -1)
2880
+ }
2881
+ }
2882
+ const search = parsed.search || ""
2883
+ return `${protocol}//${hostname}${port}${pathname}${search}`
2884
+ } catch (_) {
2885
+ return value
2886
+ }
2887
+ }
2888
+
2889
+ const formatDisplayUrl = (value) => {
2890
+ try {
2891
+ const parsed = new URL(value, location.origin)
2892
+ const host = parsed.host
2893
+ const pathname = parsed.pathname || "/"
2894
+ const search = parsed.search || ""
2895
+ return `${host}${pathname}${search}`
2896
+ } catch (_) {
2897
+ return value
2898
+ }
2899
+ }
2900
+
2901
+ const isHttpOrHttps = (value) => {
2902
+ try {
2903
+ const parsed = new URL(value, location.origin)
2904
+ const protocol = parsed.protocol.toLowerCase()
2905
+ return protocol === "http:" || protocol === "https:"
2906
+ } catch (_) {
2907
+ return false
2908
+ }
2909
+ }
2910
+
2911
+ const isHttpUrl = (value) => {
2912
+ try {
2913
+ const parsed = new URL(value, location.origin)
2914
+ return parsed.protocol.toLowerCase() === "http:"
2915
+ } catch (_) {
2916
+ return false
2917
+ }
2918
+ }
2919
+
2920
+ const isHttpsUrl = (value) => {
2921
+ try {
2922
+ const parsed = new URL(value, location.origin)
2923
+ return parsed.protocol.toLowerCase() === "https:"
2924
+ } catch (_) {
2925
+ return false
2926
+ }
2927
+ }
2928
+
2929
+ const collectUrlsFromLocal = (root) => {
2930
+ if (!root || typeof root !== "object") {
2931
+ return []
2932
+ }
2933
+ const queue = [root]
2934
+ const visited = new Set()
2935
+ const urls = new Set()
2936
+ while (queue.length > 0) {
2937
+ const current = queue.shift()
2938
+ if (!current || typeof current !== "object") {
2939
+ continue
2940
+ }
2941
+ if (visited.has(current)) {
2942
+ continue
2943
+ }
2944
+ visited.add(current)
2945
+ const values = Array.isArray(current) ? current : Object.values(current)
2946
+ for (const value of values) {
2947
+ if (typeof value === "string") {
2948
+ if (isHttpOrHttps(value)) {
2949
+ urls.add(value)
2950
+ }
2951
+ } else if (value && typeof value === "object") {
2952
+ queue.push(value)
2953
+ }
2954
+ }
2955
+ }
2956
+ return Array.from(urls)
2957
+ }
2958
+
2959
+ const collectScriptKeys = (node) => {
2960
+ const keys = new Set()
2961
+ const scriptAttr = node.getAttribute("data-script")
2962
+ if (scriptAttr) {
2963
+ const decoded = decodeURIComponent(scriptAttr)
2964
+ if (decoded) {
2965
+ keys.add(decoded)
2966
+ const withoutQuery = decoded.split("?")[0]
2967
+ if (withoutQuery) {
2968
+ keys.add(withoutQuery)
2969
+ }
2970
+ }
2971
+ }
2972
+ const filepathAttr = node.getAttribute("data-filepath")
2973
+ if (filepathAttr) {
2974
+ keys.add(filepathAttr)
2975
+ }
2976
+ return Array.from(keys)
2977
+ }
2978
+
2979
+ const ensureLocalMemory = async () => {
2980
+ const now = Date.now()
2981
+ if (!tabLinkLocalInfoPromise || now > tabLinkLocalInfoExpiry) {
2982
+ tabLinkLocalInfoPromise = fetch("/info/local", {
2983
+ method: "GET",
2984
+ headers: {
2985
+ "Accept": "application/json"
2986
+ }
2987
+ })
2988
+ .then((response) => {
2989
+ if (!response.ok) {
2990
+ throw new Error("Failed to load local info")
2991
+ }
2992
+ return response.json()
2993
+ })
2994
+ .catch(() => ({}))
2995
+ tabLinkLocalInfoExpiry = now + 3000
2996
+ }
2997
+ return tabLinkLocalInfoPromise
2998
+ }
2999
+
3000
+ const normalizeHttpsTarget = (value) => {
3001
+ if (!value || typeof value !== "string") {
3002
+ return ""
3003
+ }
3004
+ let trimmed = value.trim()
3005
+ if (!trimmed) {
3006
+ return ""
3007
+ }
3008
+ if (!/^https?:\/\//i.test(trimmed)) {
3009
+ trimmed = `https://${trimmed}`
3010
+ } else {
3011
+ trimmed = trimmed.replace(/^http:/i, "https:")
3012
+ }
3013
+ try {
3014
+ const parsed = new URL(trimmed)
3015
+ if (!parsed.host) {
3016
+ return ""
3017
+ }
3018
+ let pathname = parsed.pathname || ""
3019
+ if (pathname === "/") {
3020
+ pathname = ""
3021
+ }
3022
+ const search = parsed.search || ""
3023
+ return `https://${parsed.host}${pathname}${search}`
3024
+ } catch (_) {
3025
+ return ""
3026
+ }
3027
+ }
3028
+
3029
+ const parseHostPort = (value) => {
3030
+ if (!value || typeof value !== "string") {
3031
+ return null
3032
+ }
3033
+ let trimmed = value.trim()
3034
+ if (!trimmed) {
3035
+ return null
3036
+ }
3037
+ if (/^https?:\/\//i.test(trimmed)) {
3038
+ try {
3039
+ const parsed = new URL(trimmed)
3040
+ if (!parsed.hostname) {
3041
+ return null
3042
+ }
3043
+ const protocol = parsed.protocol.toLowerCase()
3044
+ let port = parsed.port
3045
+ if (!port) {
3046
+ if (protocol === "http:") {
3047
+ port = "80"
3048
+ } else if (protocol === "https:") {
3049
+ port = "443"
3050
+ }
3051
+ }
3052
+ if (!port) {
3053
+ return null
3054
+ }
3055
+ return {
3056
+ host: parsed.hostname.toLowerCase(),
3057
+ port
3058
+ }
3059
+ } catch (_) {
3060
+ return null
3061
+ }
3062
+ }
3063
+ const slashIndex = trimmed.indexOf("/")
3064
+ if (slashIndex >= 0) {
3065
+ trimmed = trimmed.slice(0, slashIndex)
3066
+ }
3067
+ const match = trimmed.match(/^\[?([^\]]+)\]?(?::([0-9]+))$/)
3068
+ if (!match) {
3069
+ return null
3070
+ }
3071
+ const host = match[1] ? match[1].toLowerCase() : ""
3072
+ const port = match[2] || ""
3073
+ if (!host || !port) {
3074
+ return null
3075
+ }
3076
+ return { host, port }
3077
+ }
3078
+
3079
+ const ensureRouterInfoMapping = async () => {
3080
+ const now = Date.now()
3081
+ if (!tabLinkRouterInfoPromise || now > tabLinkRouterInfoExpiry) {
3082
+ tabLinkRouterInfoPromise = fetch("/info/system", {
3083
+ method: "GET",
3084
+ headers: {
3085
+ "Accept": "application/json"
3086
+ }
3087
+ })
3088
+ .then((response) => {
3089
+ if (!response.ok) {
3090
+ throw new Error("Failed to load system info")
3091
+ }
3092
+ return response.json()
3093
+ })
3094
+ .then((data) => {
3095
+ const processes = Array.isArray(data?.router_info) ? data.router_info : []
3096
+ const rewriteMapping = data?.rewrite_mapping && typeof data.rewrite_mapping === "object"
3097
+ ? Object.values(data.rewrite_mapping)
3098
+ : []
3099
+ const portMap = new Map()
3100
+ const hostPortMap = new Map()
3101
+ const hostAliasPortMap = new Map()
3102
+ if (data?.router && typeof data.router === "object") {
3103
+ Object.entries(data.router).forEach(([dial, hosts]) => {
3104
+ const parsedDial = parseHostPort(dial)
3105
+ if (!parsedDial || !parsedDial.port) {
3106
+ return
3107
+ }
3108
+ if (!Array.isArray(hosts)) {
3109
+ return
3110
+ }
3111
+ hosts.forEach((host) => {
3112
+ if (typeof host !== "string") {
3113
+ return
3114
+ }
3115
+ const trimmed = host.trim().toLowerCase()
3116
+ if (!trimmed) {
3117
+ return
3118
+ }
3119
+ if (!hostAliasPortMap.has(trimmed)) {
3120
+ hostAliasPortMap.set(trimmed, new Set())
3121
+ }
3122
+ hostAliasPortMap.get(trimmed).add(parsedDial.port)
3123
+ })
3124
+ })
3125
+ }
3126
+ const localAliases = ["127.0.0.1", "localhost", "0.0.0.0", "::1", "[::1]"]
3127
+
3128
+ const addHttpMapping = (host, port, httpsSet) => {
3129
+ if (!host || !port || !httpsSet || httpsSet.size === 0) {
3130
+ return
3131
+ }
3132
+ const hostLower = host.toLowerCase()
3133
+ const keys = new Set([`${hostLower}:${port}`])
3134
+ if (localAliases.includes(hostLower)) {
3135
+ localAliases.forEach((alias) => keys.add(`${alias}:${port}`))
3136
+ }
3137
+ keys.forEach((key) => {
3138
+ if (!hostPortMap.has(key)) {
3139
+ hostPortMap.set(key, new Set())
3140
+ }
3141
+ const set = hostPortMap.get(key)
3142
+ httpsSet.forEach((url) => set.add(url))
3143
+ })
3144
+ if (localAliases.includes(hostLower)) {
3145
+ if (!portMap.has(port)) {
3146
+ portMap.set(port, new Set())
3147
+ }
3148
+ const portSet = portMap.get(port)
3149
+ httpsSet.forEach((url) => portSet.add(url))
3150
+ }
3151
+ }
3152
+
3153
+ const gatherHttpsTargets = (value) => {
3154
+ const targets = new Set()
3155
+ const visit = (input) => {
3156
+ if (!input) {
3157
+ return
3158
+ }
3159
+ if (Array.isArray(input)) {
3160
+ input.forEach(visit)
3161
+ return
3162
+ }
3163
+ if (typeof input === "object") {
3164
+ Object.values(input).forEach(visit)
3165
+ return
3166
+ }
3167
+ if (typeof input !== "string") {
3168
+ return
3169
+ }
3170
+ const normalized = normalizeHttpsTarget(input)
3171
+ if (normalized) {
3172
+ targets.add(normalized)
3173
+ }
3174
+ }
3175
+ visit(value)
3176
+ return targets
3177
+ }
3178
+
3179
+ const collectHostPort = (value, hostPortCandidates, portCandidates) => {
3180
+ if (!value) {
3181
+ return
3182
+ }
3183
+ if (Array.isArray(value)) {
3184
+ value.forEach((item) => collectHostPort(item, hostPortCandidates, portCandidates))
3185
+ return
3186
+ }
3187
+ if (typeof value === "object") {
3188
+ Object.values(value).forEach((item) => {
3189
+ collectHostPort(item, hostPortCandidates, portCandidates)
3190
+ })
3191
+ return
3192
+ }
3193
+ if (typeof value !== "string") {
3194
+ return
3195
+ }
3196
+ const parsed = parseHostPort(value)
3197
+ let hostLower
3198
+ if (parsed && parsed.host && parsed.port) {
3199
+ hostLower = parsed.host.toLowerCase()
3200
+ hostPortCandidates.add(`${hostLower}:${parsed.port}`)
3201
+ if (localAliases.includes(hostLower)) {
3202
+ portCandidates.add(parsed.port)
3203
+ }
3204
+ }
3205
+ const rawHost = value.replace(/^https?:\/\//i, "").split("/")[0].toLowerCase()
3206
+ const aliasPorts = hostAliasPortMap.get(rawHost)
3207
+ if (aliasPorts && aliasPorts.size > 0) {
3208
+ aliasPorts.forEach((aliasPort) => {
3209
+ hostPortCandidates.add(`${rawHost}:${aliasPort}`)
3210
+ if (localAliases.includes(rawHost)) {
3211
+ portCandidates.add(aliasPort)
3212
+ }
3213
+ })
3214
+ }
3215
+ }
3216
+
3217
+ const collectPort = (value, portCandidates) => {
3218
+ if (value === null || value === undefined || value === "") {
3219
+ return
3220
+ }
3221
+ if (Array.isArray(value)) {
3222
+ value.forEach((item) => collectPort(item, portCandidates))
3223
+ return
3224
+ }
3225
+ const port = `${value}`.trim()
3226
+ if (port && /^[0-9]+$/.test(port)) {
3227
+ portCandidates.add(port)
3228
+ }
3229
+ }
3230
+
3231
+ const registerEntry = (entry) => {
3232
+ if (!entry || typeof entry !== "object") {
3233
+ return
3234
+ }
3235
+ const httpsTargets = new Set()
3236
+ const mergeTargets = (targetValue) => {
3237
+ const targets = gatherHttpsTargets(targetValue)
3238
+ targets.forEach((url) => httpsTargets.add(url))
3239
+ }
3240
+
3241
+ mergeTargets(entry.external_router)
3242
+ mergeTargets(entry.external_domain)
3243
+ mergeTargets(entry.https_href)
3244
+ mergeTargets(entry.app_href)
3245
+ mergeTargets(entry.external_ip)
3246
+ mergeTargets(entry.internal_router)
3247
+ mergeTargets(entry.match)
3248
+ mergeTargets(entry.host)
3249
+ mergeTargets(entry.hosts)
3250
+
3251
+ if (httpsTargets.size === 0) {
3252
+ return
3253
+ }
3254
+
3255
+ const hostPortCandidates = new Set()
3256
+ const portCandidates = new Set()
3257
+
3258
+ collectHostPort(entry.external_ip, hostPortCandidates, portCandidates)
3259
+ collectHostPort(entry.internal_ip, hostPortCandidates, portCandidates)
3260
+ collectHostPort(entry.ip, hostPortCandidates, portCandidates)
3261
+ collectHostPort(entry.dial, hostPortCandidates, portCandidates)
3262
+ collectHostPort(entry.match, hostPortCandidates, portCandidates)
3263
+ collectHostPort(entry.target, hostPortCandidates, portCandidates)
3264
+ collectHostPort(entry.forward, hostPortCandidates, portCandidates)
3265
+ collectHostPort(entry.internal_router, hostPortCandidates, portCandidates)
3266
+ collectHostPort(entry.external_router, hostPortCandidates, portCandidates)
3267
+
3268
+ collectPort(entry.port, portCandidates)
3269
+ collectPort(entry.internal_port, portCandidates)
3270
+ collectPort(entry.external_port, portCandidates)
3271
+
3272
+ if (hostPortCandidates.size === 0 && portCandidates.size === 0) {
3273
+ httpsTargets.forEach((target) => {
3274
+ collectHostPort(target, hostPortCandidates, portCandidates)
3275
+ })
3276
+ }
3277
+
3278
+ if (hostPortCandidates.size === 0 && portCandidates.size === 0) {
3279
+ return
3280
+ }
3281
+
3282
+ hostPortCandidates.forEach((key) => {
3283
+ const parsed = parseHostPort(key)
3284
+ if (parsed) {
3285
+ addHttpMapping(parsed.host, parsed.port, httpsTargets)
3286
+ }
3287
+ })
3288
+
3289
+ portCandidates.forEach((port) => {
3290
+ localAliases.forEach((host) => {
3291
+ addHttpMapping(host, port, httpsTargets)
3292
+ })
3293
+ })
3294
+ }
3295
+
3296
+ const visited = new WeakSet()
3297
+ const traverseNode = (node) => {
3298
+ if (!node) {
3299
+ return
3300
+ }
3301
+ if (Array.isArray(node)) {
3302
+ node.forEach(traverseNode)
3303
+ return
3304
+ }
3305
+ if (typeof node !== "object") {
3306
+ return
3307
+ }
3308
+ if (visited.has(node)) {
3309
+ return
3310
+ }
3311
+ visited.add(node)
3312
+ registerEntry(node)
3313
+ Object.values(node).forEach((value) => {
3314
+ if (value && typeof value === "object") {
3315
+ traverseNode(value)
3316
+ }
3317
+ })
3318
+ }
3319
+
3320
+ processes.forEach(traverseNode)
3321
+ rewriteMapping.forEach(traverseNode)
3322
+
3323
+ return {
3324
+ portMap,
3325
+ hostPortMap
3326
+ }
3327
+ })
3328
+ .catch(() => ({
3329
+ portMap: new Map(),
3330
+ hostPortMap: new Map()
3331
+ }))
3332
+ tabLinkRouterInfoExpiry = now + 3000
3333
+ }
3334
+ return tabLinkRouterInfoPromise
3335
+ }
3336
+
3337
+ const collectHttpsUrlsFromRouter = (httpUrl, routerData) => {
3338
+ if (!routerData) {
3339
+ return []
3340
+ }
3341
+ let parsed
3342
+ try {
3343
+ parsed = new URL(httpUrl, location.origin)
3344
+ } catch (_) {
3345
+ return []
3346
+ }
3347
+ const protocol = parsed.protocol.toLowerCase()
3348
+ if (protocol !== "http:" && protocol !== "https:") {
3349
+ return []
3350
+ }
3351
+ let port = parsed.port
3352
+ if (!port) {
3353
+ if (protocol === "http:") {
3354
+ port = "80"
3355
+ } else if (protocol === "https:") {
3356
+ port = "443"
3357
+ }
3358
+ }
3359
+ const hostLower = parsed.hostname.toLowerCase()
3360
+ const results = new Set()
3361
+ if (port) {
3362
+ const hostPortKey = `${hostLower}:${port}`
3363
+ if (routerData.hostPortMap.has(hostPortKey)) {
3364
+ routerData.hostPortMap.get(hostPortKey).forEach((value) => results.add(value))
3365
+ }
3366
+ if (routerData.portMap.has(port)) {
3367
+ routerData.portMap.get(port).forEach((value) => results.add(value))
3368
+ }
3369
+ }
3370
+ return Array.from(results)
3371
+ }
3372
+
3373
+ const buildTabLinkEntries = async (link) => {
3374
+ if (!link || !link.href) {
3375
+ return []
3376
+ }
3377
+
3378
+ const baseHref = link.href
3379
+ const entries = []
3380
+ const entryByUrl = new Map()
3381
+ const addEntry = (type, label, url) => {
3382
+ if (!url) {
3383
+ return
3384
+ }
3385
+ const canonical = canonicalizeUrl(url)
3386
+ if (!canonical) {
3387
+ return
3388
+ }
3389
+ if (entryByUrl.has(canonical)) {
3390
+ return
3391
+ }
3392
+ const entry = {
3393
+ type,
3394
+ label,
3395
+ url: canonical,
3396
+ display: formatDisplayUrl(canonical)
3397
+ }
3398
+ entryByUrl.set(canonical, entry)
3399
+ entries.push(entry)
3400
+ }
3401
+
3402
+ if (isHttpUrl(baseHref)) {
3403
+ addEntry("http", "HTTP", baseHref)
3404
+ } else if (isHttpsUrl(baseHref)) {
3405
+ addEntry("https", "HTTPS", baseHref)
3406
+ } else {
3407
+ addEntry("url", "URL", baseHref)
3408
+ }
3409
+
3410
+ const httpCandidates = new Set()
3411
+ const httpsCandidates = new Set()
3412
+
3413
+ if (isHttpUrl(baseHref)) {
3414
+ httpCandidates.add(canonicalizeUrl(baseHref))
3415
+ } else if (isHttpsUrl(baseHref)) {
3416
+ httpsCandidates.add(canonicalizeUrl(baseHref))
3417
+ }
3418
+
3419
+ const scriptKeys = collectScriptKeys(link)
3420
+ if (scriptKeys.length > 0) {
3421
+ const localInfo = await ensureLocalMemory()
3422
+ scriptKeys.forEach((key) => {
3423
+ if (!key) {
3424
+ return
3425
+ }
3426
+ const local = localInfo ? localInfo[key] : undefined
3427
+ if (!local) {
3428
+ return
3429
+ }
3430
+ const urls = collectUrlsFromLocal(local)
3431
+ urls.forEach((value) => {
3432
+ const canonical = canonicalizeUrl(value)
3433
+ if (isHttpsUrl(canonical)) {
3434
+ httpsCandidates.add(canonical)
3435
+ } else if (isHttpUrl(canonical)) {
3436
+ httpCandidates.add(canonical)
3437
+ }
3438
+ })
3439
+ })
3440
+ }
3441
+
3442
+ if (httpCandidates.size > 0) {
3443
+ const routerData = await ensureRouterInfoMapping()
3444
+ httpCandidates.forEach((httpUrl) => {
3445
+ const mapped = collectHttpsUrlsFromRouter(httpUrl, routerData)
3446
+ mapped.forEach((httpsUrl) => {
3447
+ httpsCandidates.add(httpsUrl)
3448
+ })
3449
+ })
3450
+ }
3451
+
3452
+ const httpList = Array.from(httpCandidates).sort()
3453
+ const httpsList = Array.from(httpsCandidates).sort()
3454
+
3455
+ httpList.forEach((url) => {
3456
+ addEntry("http", "HTTP", url)
3457
+ })
3458
+ httpsList.forEach((url) => {
3459
+ addEntry("https", "HTTPS", url)
3460
+ })
3461
+
3462
+ return entries
3463
+ }
3464
+
3465
+ const positionTabLinkPopover = (popover, link) => {
3466
+ if (!popover || !link) {
3467
+ return
3468
+ }
3469
+ const rect = link.getBoundingClientRect()
3470
+ const minWidth = Math.max(rect.width, 260)
3471
+ popover.style.minWidth = `${Math.round(minWidth)}px`
3472
+ popover.style.display = "flex"
3473
+ popover.classList.add("visible")
3474
+ popover.style.visibility = "hidden"
3475
+
3476
+ const popoverWidth = popover.offsetWidth
3477
+ const popoverHeight = popover.offsetHeight
3478
+
3479
+ let left = rect.left
3480
+ let top = rect.bottom + 8
3481
+
3482
+ if (left + popoverWidth > window.innerWidth - 12) {
3483
+ left = window.innerWidth - popoverWidth - 12
3484
+ }
3485
+ if (left < 12) {
3486
+ left = 12
3487
+ }
3488
+
3489
+ if (top + popoverHeight > window.innerHeight - 12) {
3490
+ top = Math.max(12, rect.top - popoverHeight - 8)
3491
+ }
3492
+
3493
+ popover.style.left = `${Math.round(left)}px`
3494
+ popover.style.top = `${Math.round(top)}px`
3495
+ popover.style.visibility = ""
3496
+ }
3497
+
3498
+ const hideTabLinkPopover = ({ immediate = false } = {}) => {
3499
+ const applyHide = () => {
3500
+ if (tabLinkPopoverEl) {
3501
+ tabLinkPopoverEl.classList.remove("visible")
3502
+ tabLinkPopoverEl.style.display = "none"
3503
+ }
3504
+ tabLinkActiveLink = null
3505
+ tabLinkPendingLink = null
3506
+ tabLinkHideTimer = null
3507
+ }
3508
+
3509
+ if (tabLinkHideTimer) {
3510
+ clearTimeout(tabLinkHideTimer)
3511
+ tabLinkHideTimer = null
3512
+ }
3513
+
3514
+ if (immediate) {
3515
+ applyHide()
3516
+ } else {
3517
+ tabLinkHideTimer = setTimeout(applyHide, 120)
3518
+ }
3519
+ }
3520
+
3521
+ const renderTabLinkPopover = async (link) => {
3522
+ if (!link || !link.href) {
3523
+ hideTabLinkPopover({ immediate: true })
3524
+ return
3525
+ }
3526
+
3527
+ try {
3528
+ const linkOrigin = new URL(link.href, location.href).origin
3529
+ if (linkOrigin === location.origin) {
3530
+ hideTabLinkPopover({ immediate: true })
3531
+ return
3532
+ }
3533
+ } catch (_) {
3534
+ hideTabLinkPopover({ immediate: true })
3535
+ return
3536
+ }
3537
+
3538
+ if (tabLinkActiveLink === link && tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible")) {
3539
+ if (tabLinkHideTimer) {
3540
+ clearTimeout(tabLinkHideTimer)
3541
+ tabLinkHideTimer = null
3542
+ }
3543
+ return
3544
+ }
3545
+
3546
+ if (tabLinkPendingLink === link && tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible")) {
3547
+ return
3548
+ }
3549
+
3550
+ tabLinkPendingLink = link
3551
+ if (tabLinkHideTimer) {
3552
+ clearTimeout(tabLinkHideTimer)
3553
+ tabLinkHideTimer = null
3554
+ }
3555
+
3556
+ let entries
3557
+ try {
3558
+ entries = await buildTabLinkEntries(link)
3559
+ } catch (_) {
3560
+ tabLinkPendingLink = null
3561
+ return
3562
+ }
3563
+
3564
+ if (tabLinkPendingLink !== link) {
3565
+ return
3566
+ }
3567
+
3568
+ if (!entries || entries.length === 0) {
3569
+ hideTabLinkPopover({ immediate: true })
3570
+ return
3571
+ }
3572
+
3573
+ const popover = ensureTabLinkPopoverEl()
3574
+ popover.innerHTML = ""
3575
+
3576
+ const header = document.createElement("div")
3577
+ header.className = "tab-link-popover-header"
3578
+ header.innerHTML = `<i class="fa-solid fa-arrow-up-right-from-square"></i><span>Open in browser</span>`
3579
+ popover.appendChild(header)
3580
+
3581
+ entries.forEach((entry) => {
3582
+ const item = document.createElement("button")
3583
+ item.type = "button"
3584
+ item.className = "tab-link-popover-item"
3585
+ item.setAttribute("data-url", entry.url)
3586
+ const labelSpan = document.createElement("span")
3587
+ labelSpan.className = "label"
3588
+ labelSpan.textContent = entry.label
3589
+ const valueSpan = document.createElement("span")
3590
+ valueSpan.className = "value"
3591
+ valueSpan.textContent = entry.display
3592
+ item.append(labelSpan, valueSpan)
3593
+ popover.appendChild(item)
3594
+ })
3595
+
3596
+ tabLinkActiveLink = link
3597
+ tabLinkPendingLink = null
3598
+ positionTabLinkPopover(popover, link)
3599
+ }
3600
+
3601
+ const setupTabLinkHover = () => {
3602
+ const container = document.querySelector(".appcanvas > aside .menu-container")
3603
+ if (!container) {
3604
+ return
3605
+ }
3606
+
3607
+ container.addEventListener("mouseover", (event) => {
3608
+ const link = event.target.closest(".frame-link")
3609
+ if (!link || !container.contains(link)) {
3610
+ return
3611
+ }
3612
+ renderTabLinkPopover(link)
3613
+ })
3614
+
3615
+ container.addEventListener("mouseout", (event) => {
3616
+ const origin = event.target.closest(".frame-link")
3617
+ if (!origin || !container.contains(origin)) {
3618
+ return
3619
+ }
3620
+ const related = event.relatedTarget
3621
+ const popover = tabLinkPopoverEl || document.getElementById(TAB_LINK_POPOVER_ID)
3622
+ if (related && (origin.contains(related) || (popover && popover.contains(related)))) {
3623
+ return
3624
+ }
3625
+ hideTabLinkPopover()
3626
+ })
3627
+ }
3628
+
3629
+ const handleGlobalPointer = (event) => {
3630
+ if (!tabLinkPopoverEl || !tabLinkPopoverEl.classList.contains("visible")) {
3631
+ return
3632
+ }
3633
+ if (tabLinkPopoverEl.contains(event.target)) {
3634
+ return
3635
+ }
3636
+ if (tabLinkActiveLink && tabLinkActiveLink.contains(event.target)) {
3637
+ return
3638
+ }
3639
+ hideTabLinkPopover({ immediate: true })
3640
+ }
3641
+
3642
+ window.addEventListener("scroll", () => {
3643
+ if (tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible")) {
3644
+ hideTabLinkPopover({ immediate: true })
3645
+ }
3646
+ }, true)
3647
+
3648
+ window.addEventListener("resize", () => {
3649
+ if (tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible") && tabLinkActiveLink) {
3650
+ positionTabLinkPopover(tabLinkPopoverEl, tabLinkActiveLink)
3651
+ }
3652
+ })
3653
+
3654
+ document.addEventListener("mousedown", handleGlobalPointer, true)
3655
+ try {
3656
+ document.addEventListener("touchstart", handleGlobalPointer, { passive: true, capture: true })
3657
+ } catch (_) {
3658
+ document.addEventListener("touchstart", handleGlobalPointer, true)
3659
+ }
2725
3660
  const getWindowStorage = () => {
2726
3661
  if (typeof windowStorage === "undefined" || !windowStorage) {
2727
3662
  return null
@@ -3938,14 +4873,6 @@ body.dark {
3938
4873
  })
3939
4874
  })
3940
4875
  }
3941
- document.querySelector("#popout-link").addEventListener("click", async (e) => {
3942
- e.preventDefault()
3943
- e.stopPropagation()
3944
- let iframe = document.querySelector("iframe:not(.hidden)")
3945
- if (iframe) {
3946
- open_url(iframe.src, "_blank")
3947
- }
3948
- })
3949
4876
  if (document.querySelector("#edit")) {
3950
4877
  document.querySelector("#edit").addEventListener("click", async (e) => {
3951
4878
  e.preventDefault()
@@ -4283,6 +5210,10 @@ body.dark {
4283
5210
  return
4284
5211
  }
4285
5212
 
5213
+ if (e.target.closest(".frame-link")) {
5214
+ hideTabLinkPopover({ immediate: true })
5215
+ }
5216
+
4286
5217
  // 1. handle delete
4287
5218
  if (e.target.classList.contains("del")) {
4288
5219
  target = e.target
@@ -4293,6 +5224,7 @@ body.dark {
4293
5224
  if (target) {
4294
5225
  e.preventDefault()
4295
5226
  e.stopPropagation()
5227
+ hideTabLinkPopover({ immediate: true })
4296
5228
  // Delete
4297
5229
  // delete link
4298
5230
  let el = target.closest(".frame-link")
@@ -4310,7 +5242,7 @@ body.dark {
4310
5242
  }
4311
5243
 
4312
5244
 
4313
- // 2. handle shudown
5245
+ // 2. handle shutdown
4314
5246
  if (e.target.classList.contains("shutdown")) {
4315
5247
  target = e.target
4316
5248
  } else {
@@ -4539,6 +5471,7 @@ body.dark {
4539
5471
  container.addEventListener("click", handleMenuClick)
4540
5472
  }
4541
5473
  })
5474
+ setupTabLinkHover()
4542
5475
  document.addEventListener("click", (event) => {
4543
5476
  if (event.target.closest("#fs-status .fs-dropdown-menu")) {
4544
5477
  return
@@ -948,7 +948,6 @@ body.dark .top-menu .btn2.selected {
948
948
  <a class="btn2 <%=type === 'browse' ? 'selected' : ''%>" href="<%=dev_tab%>"><div><i class="fa-solid fa-code"></i></div><div>Dev</div></a>
949
949
  <a class="btn2 <%=type === 'run' ? 'selected' : ''%>" href="<%=run_tab%>"><div><i class="fa-solid fa-circle-play"></i></div><div>Run</div></a>
950
950
  </div>
951
- <button class='btn2' id='popout-link' data-tippy-content="open in a web browser"><i class="fa-solid fa-arrow-up-right-from-square"></i></button>
952
951
  <a class='btn2' href="/columns" data-tippy-content="split into 2 columns">
953
952
  <div><i class="fa-solid fa-table-columns"></i></div>
954
953
  </a>