Skip to content

Commit d9a793b

Browse files
committed
feat(hub): add iframe navigation recovery controls
1 parent a55f3d5 commit d9a793b

5 files changed

Lines changed: 66 additions & 33 deletions

File tree

docs/content/8.references/6.hub-api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ The built-in variants of the open dock union (`DevframeDockEntryRegistry`, `@dev
137137

138138
| Type | The hub UI provider renders |
139139
|---|---|
140-
| `iframe` | the entry's `url` in a kept-alive iframe (per `frameId` when shared); honor `subTabs` soft nav |
140+
| `iframe` | the entry's `url` in a kept-alive iframe (per `frameId` when shared); honor `subTabs` soft nav; force the existing address bar with `showAddressBar`; override Back, Reload, and Open externally visibility with `controls.back`, `controls.reload`, and `controls.openExternal` |
141141
| `action` | a dock-rail button; activating runs its client script |
142142
| `custom-render` | a container its client script mounts into |
143143
| `launcher` | a launch call-to-action reflecting `launcher.status` |

packages/hub-ui/src/client/components/views/ViewIframe.vue

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const props = defineProps<{
2020
2121
const settings = useSettings(props.context)
2222
const isEdgeMode = computed(() => props.context.panel.store.mode === 'edge')
23-
const showAddressBar = computed(() => settings.value.showIframeAddressBar)
23+
const showAddressBar = computed(() => settings.value.showIframeAddressBar || props.entry.showAddressBar === true)
2424
2525
const ADDRESS_BAR_HEIGHT = 40
2626
@@ -77,13 +77,15 @@ const currentPageOrigin = computed(() => {
7777
// Check if iframe URL is cross-origin
7878
const isCrossOrigin = computed(() => {
7979
try {
80-
const url = new URL(currentUrl.value)
81-
return url.origin !== currentPageOrigin.value
80+
return new URL(currentUrl.value).origin !== currentPageOrigin.value
8281
}
8382
catch {
8483
return true // Assume cross-origin if URL parsing fails
8584
}
8685
})
86+
const showBack = computed(() => props.entry.controls?.back ?? !isCrossOrigin.value)
87+
const showReload = computed(() => props.entry.controls?.reload ?? !isCrossOrigin.value)
88+
const showOpenExternal = computed(() => props.entry.controls?.openExternal ?? false)
8789
8890
// Display URL - hides host if same as current page. The remote connection
8991
// descriptor is stripped so its auth token can't be read (or copied) out of the
@@ -189,10 +191,20 @@ function refresh() {
189191
190192
assetsError.value = null
191193
isIframeLoading.value = true
192-
// Reload by reassigning the src
193194
const src = iframe.src
194195
iframe.src = ''
195196
iframe.src = src
197+
currentUrl.value = src
198+
editingUrl.value = src
199+
}
200+
201+
function openExternally() {
202+
try {
203+
const url = new URL(stripRemoteConnectionFromUrl(currentUrl.value))
204+
if (url.protocol === 'http:' || url.protocol === 'https:')
205+
window.open(url.href, '_blank', 'noopener,noreferrer')
206+
}
207+
catch {}
196208
}
197209
198210
let onIframeLoad: (() => void) | undefined
@@ -322,40 +334,37 @@ onUnmounted(() => {
322334
<div class="w-full h-full flex flex-col">
323335
<div
324336
v-if="showAddressBar"
325-
class="flex-none px-2 w-full flex items-center gap-1 border-base border-b"
337+
class="flex-none px-2 w-full flex items-center gap-1 color-base border-base border-b"
326338
:style="{ height: `${ADDRESS_BAR_HEIGHT}px` }"
327339
>
328-
<!-- Navigation buttons (hidden for cross-origin) -->
329-
<template v-if="!isCrossOrigin">
330-
<!-- Back button -->
331-
<button
332-
class="w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0"
333-
title="Back"
334-
@click="goBack"
335-
>
336-
<div class="i-ph-caret-left op60 w-4.5 h-4.5" />
337-
</button>
338-
339-
<!-- Refresh button -->
340-
<button
341-
class="w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0"
342-
title="Refresh"
343-
@click="refresh"
344-
>
345-
<div class="i-ph-arrow-clockwise op60 w-4.5 h-4.5" />
346-
</button>
347-
</template>
340+
<button
341+
v-if="showBack"
342+
class="w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0"
343+
title="Back"
344+
@click="goBack"
345+
>
346+
<div class="i-ph-caret-left op60 w-4.5 h-4.5" />
347+
</button>
348348

349349
<!-- Cross-origin badge -->
350350
<div
351-
v-else
351+
v-if="isCrossOrigin"
352352
class="flex items-center gap-1 px2 py1 rounded text-xs bg-amber/10 text-amber border border-amber/20 shrink-0"
353-
title="Cross-origin iframe - navigation controls unavailable"
353+
title="Cross-origin iframe"
354354
>
355355
<div class="i-ph-globe text-sm" />
356356
<span>Cross-Origin</span>
357357
</div>
358358

359+
<button
360+
v-if="showReload"
361+
class="w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0"
362+
title="Reload"
363+
@click="refresh"
364+
>
365+
<div class="i-ph-arrow-clockwise op60 w-4.5 h-4.5" />
366+
</button>
367+
359368
<!-- URL input -->
360369
<div class="flex-1 flex items-center h-7 px-2.5 rounded bg-gray/5 border border-transparent hover:border-gray/10 focus-within:border-gray/15 transition-colors">
361370
<input
@@ -376,6 +385,15 @@ onUnmounted(() => {
376385
class="i-ph-circle-notch text-sm op40 ml-2 shrink-0 animate-spin"
377386
/>
378387
</div>
388+
389+
<button
390+
v-if="showOpenExternal"
391+
class="w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0"
392+
title="Open externally"
393+
@click="openExternally"
394+
>
395+
<div class="i-ph-arrow-square-out-duotone op60 w-4.5 h-4.5" />
396+
</button>
379397
</div>
380398
<div
381399
ref="viewFrame"

packages/hub-ui/src/client/components/views/ViewIframeLoading.vue

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
<script setup lang="ts">
2-
// Placeholder shown while an iframe view loads its content. A blank iframe
3-
// paints white during load, so this is only visible once the pane steps aside
4-
// (`pane.hide()` in `ViewIframe`) — the same layering trick `ViewAssetsError`
5-
// relies on. It covers the initial load and any hard navigation/refresh.
2+
// Placeholder shown while an iframe view loads its content. The pane steps
3+
// aside while loading so this inherits the viewer's configured background.
64
</script>
75

86
<template>
9-
<div class="devframes-view-iframe-loading absolute inset-0 flex flex-col items-center justify-center gap-2 bg-base">
7+
<div class="devframes-view-iframe-loading absolute inset-0 flex flex-col items-center justify-center gap-2">
108
<div class="i-ph:circle-notch-duotone animate-spin text-3xl color-faint" />
119
<div class="text-sm color-muted">
1210
Loading…

packages/hub/src/types/docks.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@ declare module 'devframe/types' {
203203
export interface DevframeViewIframe extends DevframeDockEntryBase {
204204
type: 'iframe'
205205
url: string
206+
/** Show the iframe address bar regardless of the user's global setting. */
207+
showAddressBar?: boolean
208+
/** Optional address-bar controls. Omitted values retain the existing origin-based behavior. */
209+
controls?: {
210+
/** Override Back visibility. Cross-origin history access may still be blocked by the browser. */
211+
back?: boolean
212+
/** Override Reload visibility. */
213+
reload?: boolean
214+
/** Override the built-in action that opens the displayed HTTP(S) URL in a new tab. */
215+
openExternal?: boolean
216+
}
206217
/**
207218
* The id of the iframe, if multiple tabs is assigned with the same id, the iframe will be shared.
208219
*

tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,12 @@ export interface DevframeViewGroup extends DevframeDockEntryBase {
293293
export interface DevframeViewIframe extends DevframeDockEntryBase {
294294
type: 'iframe';
295295
url: string;
296+
showAddressBar?: boolean;
297+
controls?: {
298+
back?: boolean;
299+
reload?: boolean;
300+
openExternal?: boolean;
301+
};
296302
frameId?: string;
297303
clientScript?: ClientScriptEntry;
298304
navTarget?: NavTarget;

0 commit comments

Comments
 (0)