Skip to content

Commit 1eba745

Browse files
authored
feat(hub): add configurable iframe navigation controls (#318)
1 parent 2568baf commit 1eba745

6 files changed

Lines changed: 67 additions & 36 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; show the existing address bar with `addressBar: true`, or configure Back, Reload, and Open externally by passing an `addressBar` object |
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-builtin/SettingsAppearance.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ function setDockMode(mode: string) {
105105
/>
106106
</button>
107107
<div class="flex flex-col">
108-
<span class="text-sm">Show iframe address bar</span>
109-
<span class="text-xs op50">Display navigation controls and URL bar for iframe views</span>
108+
<span class="text-sm">Always show iframe address bars</span>
109+
<span class="text-xs op50">Show the address bar for every iframe dock.</span>
110110
</div>
111111
</label>
112112

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

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ 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 addressBarControls = computed(() => typeof props.entry.addressBar === 'object' ? props.entry.addressBar : undefined)
24+
const showAddressBar = computed(() => settings.value.showIframeAddressBar || Boolean(props.entry.addressBar))
2425
2526
const ADDRESS_BAR_HEIGHT = 40
2627
@@ -77,13 +78,15 @@ const currentPageOrigin = computed(() => {
7778
// Check if iframe URL is cross-origin
7879
const isCrossOrigin = computed(() => {
7980
try {
80-
const url = new URL(currentUrl.value)
81-
return url.origin !== currentPageOrigin.value
81+
return new URL(currentUrl.value).origin !== currentPageOrigin.value
8282
}
8383
catch {
8484
return true // Assume cross-origin if URL parsing fails
8585
}
8686
})
87+
const showBack = computed(() => addressBarControls.value?.back ?? !isCrossOrigin.value)
88+
const showReload = computed(() => addressBarControls.value?.reload ?? !isCrossOrigin.value)
89+
const showOpenExternal = computed(() => addressBarControls.value?.openExternal ?? false)
8790
8891
// Display URL - hides host if same as current page. The remote connection
8992
// descriptor is stripped so its auth token can't be read (or copied) out of the
@@ -189,10 +192,20 @@ function refresh() {
189192
190193
assetsError.value = null
191194
isIframeLoading.value = true
192-
// Reload by reassigning the src
193-
const src = iframe.src
195+
const src = currentUrl.value
194196
iframe.src = ''
195197
iframe.src = src
198+
currentUrl.value = src
199+
editingUrl.value = src
200+
}
201+
202+
function openExternally() {
203+
try {
204+
const url = new URL(stripRemoteConnectionFromUrl(currentUrl.value))
205+
if (url.protocol === 'http:' || url.protocol === 'https:')
206+
window.open(url.href, '_blank', 'noopener,noreferrer')
207+
}
208+
catch {}
196209
}
197210
198211
let onIframeLoad: (() => void) | undefined
@@ -322,40 +335,37 @@ onUnmounted(() => {
322335
<div class="w-full h-full flex flex-col">
323336
<div
324337
v-if="showAddressBar"
325-
class="flex-none px-2 w-full flex items-center gap-1 border-base border-b"
338+
class="flex-none px-2 w-full flex items-center gap-1 color-base border-base border-b"
326339
:style="{ height: `${ADDRESS_BAR_HEIGHT}px` }"
327340
>
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>
341+
<button
342+
v-if="showBack"
343+
class="w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0"
344+
title="Back"
345+
@click="goBack"
346+
>
347+
<div class="i-ph-caret-left op60 w-4.5 h-4.5" />
348+
</button>
348349

349350
<!-- Cross-origin badge -->
350351
<div
351-
v-else
352+
v-if="isCrossOrigin"
352353
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"
354+
title="Cross-origin iframe"
354355
>
355356
<div class="i-ph-globe text-sm" />
356357
<span>Cross-Origin</span>
357358
</div>
358359

360+
<button
361+
v-if="showReload"
362+
class="w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0"
363+
title="Reload"
364+
@click="refresh"
365+
>
366+
<div class="i-ph-arrow-clockwise op60 w-4.5 h-4.5" />
367+
</button>
368+
359369
<!-- URL input -->
360370
<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">
361371
<input
@@ -376,6 +386,15 @@ onUnmounted(() => {
376386
class="i-ph-circle-notch text-sm op40 ml-2 shrink-0 animate-spin"
377387
/>
378388
</div>
389+
390+
<button
391+
v-if="showOpenExternal"
392+
class="w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0"
393+
title="Open externally"
394+
@click="openExternally"
395+
>
396+
<div class="i-ph-arrow-square-out-duotone op60 w-4.5 h-4.5" />
397+
</button>
379398
</div>
380399
<div
381400
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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,15 @@ declare module 'devframe/types' {
209209
export interface DevframeViewIframe extends DevframeDockEntryBase {
210210
type: 'iframe'
211211
url: string
212+
/** Request the address bar for this iframe dock. Pass an object to configure its controls. The user's global always-show setting forces it on for every iframe. */
213+
addressBar?: boolean | {
214+
/** Override Back visibility. Cross-origin history access may still be blocked by the browser. */
215+
back?: boolean
216+
/** Override Reload visibility. */
217+
reload?: boolean
218+
/** Override Open externally visibility. */
219+
openExternal?: boolean
220+
}
212221
/**
213222
* The id of the iframe, if multiple tabs is assigned with the same id, the iframe will be shared.
214223
*

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,11 @@ export interface DevframeViewGroup extends DevframeDockEntryBase {
308308
export interface DevframeViewIframe extends DevframeDockEntryBase {
309309
type: 'iframe';
310310
url: string;
311+
addressBar?: boolean | {
312+
back?: boolean;
313+
reload?: boolean;
314+
openExternal?: boolean;
315+
};
311316
frameId?: string;
312317
clientScript?: ClientScriptEntry;
313318
navTarget?: NavTarget;

0 commit comments

Comments
 (0)